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

lephilousophe noreply at scummvm.org
Fri Jan 20 13:14:15 UTC 2023


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

Summary:
787d4e1db6 POSIX: Allow to subclass DrivePOSIXFilesystemNode
54fd20c36c POSIX: Refactor DrivePOSIXFilesystemNode to allow for dynamic drives
e17b34c9dc ANDROID: Store JNI environment in a thread local variable
c84c9cd313 ANDROID: Update build tools
9631567923 ANDROID: Add a method to get running SDK version
c60ad0a554 ANDROID: Various cleanups in JNI
0fdf53f984 ANDROID: Remove platform specific code from shared code
ca1dbfc9d6 ANDROID: Add SAF support
f939e442bb ANDROID: Increase SDK version
a465718c24 ANDROID: Fix comment
f78e79fd4f ANDROID: Clear SAF cache when activity is hidden
baf42ae7e6 ANDROID: Add a dialog to revoke SAF authorizations


Commit: 787d4e1db612191b0f8ad6ee959096b6eb292d5b
    https://github.com/scummvm/scummvm/commit/787d4e1db612191b0f8ad6ee959096b6eb292d5b
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2023-01-20T14:14:03+01:00

Commit Message:
POSIX: Allow to subclass DrivePOSIXFilesystemNode

Without this, there are some cases where the nodes created are not the
subclass.
Also make _config available to derived classes.

Changed paths:
    backends/fs/posix-drives/posix-drives-fs.cpp
    backends/fs/posix-drives/posix-drives-fs.h


diff --git a/backends/fs/posix-drives/posix-drives-fs.cpp b/backends/fs/posix-drives/posix-drives-fs.cpp
index 257825de45c..a9f1be247cb 100644
--- a/backends/fs/posix-drives/posix-drives-fs.cpp
+++ b/backends/fs/posix-drives/posix-drives-fs.cpp
@@ -110,7 +110,7 @@ DrivePOSIXFilesystemNode *DrivePOSIXFilesystemNode::getChildWithKnownType(const
 		newPath += '/';
 	newPath += n;
 
-	DrivePOSIXFilesystemNode *child = new DrivePOSIXFilesystemNode(_config);
+	DrivePOSIXFilesystemNode *child = reinterpret_cast<DrivePOSIXFilesystemNode *>(makeNode());
 	child->_path = newPath;
 	child->_isValid = true;
 	child->_isPseudoRoot = false;
@@ -187,8 +187,7 @@ AbstractFSNode *DrivePOSIXFilesystemNode::getParent() const {
 	}
 
 	if (isDrive(_path)) {
-		DrivePOSIXFilesystemNode *root = new DrivePOSIXFilesystemNode(_config);
-		return root;
+		return makeNode();
 	}
 
 	return POSIXFilesystemNode::getParent();
diff --git a/backends/fs/posix-drives/posix-drives-fs.h b/backends/fs/posix-drives/posix-drives-fs.h
index cda2e54c31c..5af57c66088 100644
--- a/backends/fs/posix-drives/posix-drives-fs.h
+++ b/backends/fs/posix-drives/posix-drives-fs.h
@@ -32,6 +32,9 @@ class StdioStream;
  */
 class DrivePOSIXFilesystemNode : public POSIXFilesystemNode {
 protected:
+	virtual AbstractFSNode *makeNode() const {
+		return new DrivePOSIXFilesystemNode(_config);
+	}
 	AbstractFSNode *makeNode(const Common::String &path) const override {
 		return new DrivePOSIXFilesystemNode(path, _config);
 	}
@@ -66,9 +69,11 @@ public:
 	bool getChildren(AbstractFSList &list, ListMode mode, bool hidden) const override;
 	AbstractFSNode *getParent() const override;
 
+protected:
+	const Config &_config;
+
 private:
 	bool _isPseudoRoot;
-	const Config &_config;
 
 	DrivePOSIXFilesystemNode *getChildWithKnownType(const Common::String &n, bool isDirectoryFlag) const;
 	bool isDrive(const Common::String &path) const;


Commit: 54fd20c36c3e975fd862bf42448a74a83caf4fbf
    https://github.com/scummvm/scummvm/commit/54fd20c36c3e975fd862bf42448a74a83caf4fbf
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2023-01-20T14:14:03+01:00

Commit Message:
POSIX: Refactor DrivePOSIXFilesystemNode to allow for dynamic drives

Changed paths:
    backends/fs/posix-drives/posix-drives-fs-factory.cpp
    backends/fs/posix-drives/posix-drives-fs-factory.h
    backends/fs/posix-drives/posix-drives-fs.cpp
    backends/fs/posix-drives/posix-drives-fs.h


diff --git a/backends/fs/posix-drives/posix-drives-fs-factory.cpp b/backends/fs/posix-drives/posix-drives-fs-factory.cpp
index dcd883b280a..879e79d5b3f 100644
--- a/backends/fs/posix-drives/posix-drives-fs-factory.cpp
+++ b/backends/fs/posix-drives/posix-drives-fs-factory.cpp
@@ -55,4 +55,16 @@ AbstractFSNode *DrivesPOSIXFilesystemFactory::makeFileNodePath(const Common::Str
 	return new DrivePOSIXFilesystemNode(path, _config);
 }
 
+bool DrivesPOSIXFilesystemFactory::StaticDrivesConfig::getDrives(AbstractFSList &list, bool hidden) const {
+	for (uint i = 0; i < drives.size(); i++) {
+		list.push_back(_factory->makeFileNodePath(drives[i]));
+	}
+	return true;
+}
+
+bool DrivesPOSIXFilesystemFactory::StaticDrivesConfig::isDrive(const Common::String &path) const {
+	DrivesArray::const_iterator drive = Common::find(drives.begin(), drives.end(), path);
+	return drive != drives.end();
+}
+
 #endif
diff --git a/backends/fs/posix-drives/posix-drives-fs-factory.h b/backends/fs/posix-drives/posix-drives-fs-factory.h
index 754e0c4ebc6..11633f7b66d 100644
--- a/backends/fs/posix-drives/posix-drives-fs-factory.h
+++ b/backends/fs/posix-drives/posix-drives-fs-factory.h
@@ -36,6 +36,8 @@
  */
 class DrivesPOSIXFilesystemFactory : public FilesystemFactory {
 public:
+	DrivesPOSIXFilesystemFactory() : _config(this) { }
+
 	/**
 	 * Add a drive to the top-level directory
 	 */
@@ -56,7 +58,20 @@ protected:
 	AbstractFSNode *makeFileNodePath(const Common::String &path) const override;
 
 private:
-	DrivePOSIXFilesystemNode::Config _config;
+	typedef Common::Array<Common::String> DrivesArray;
+	struct StaticDrivesConfig : public DrivePOSIXFilesystemNode::Config {
+		StaticDrivesConfig(const DrivesPOSIXFilesystemFactory *factory) : _factory(factory) { }
+
+		bool getDrives(AbstractFSList &list, bool hidden) const override;
+		bool isDrive(const Common::String &path) const override;
+
+		DrivesArray drives;
+
+	private:
+		const DrivesPOSIXFilesystemFactory *_factory;
+	};
+
+	StaticDrivesConfig _config;
 };
 
 #endif
diff --git a/backends/fs/posix-drives/posix-drives-fs.cpp b/backends/fs/posix-drives/posix-drives-fs.cpp
index a9f1be247cb..861ecb1d3c6 100644
--- a/backends/fs/posix-drives/posix-drives-fs.cpp
+++ b/backends/fs/posix-drives/posix-drives-fs.cpp
@@ -31,11 +31,6 @@
 
 #include <dirent.h>
 
-DrivePOSIXFilesystemNode::Config::Config() {
-	bufferingMode = kBufferingModeStdio;
-	bufferSize = 0; // Use the default stdio buffer size
-}
-
 DrivePOSIXFilesystemNode::DrivePOSIXFilesystemNode(const Common::String &path, const Config &config) :
 		POSIXFilesystemNode(path),
 		_isPseudoRoot(false),
@@ -131,11 +126,7 @@ bool DrivePOSIXFilesystemNode::getChildren(AbstractFSList &list, AbstractFSNode:
 	assert(_isDirectory);
 
 	if (_isPseudoRoot) {
-		for (uint i = 0; i < _config.drives.size(); i++) {
-			list.push_back(makeNode(_config.drives[i]));
-		}
-
-		return true;
+		return _config.getDrives(list, hidden);
 	} else {
 		DIR *dirp = opendir(_path.c_str());
 		struct dirent *dp;
@@ -195,9 +186,7 @@ AbstractFSNode *DrivePOSIXFilesystemNode::getParent() const {
 
 bool DrivePOSIXFilesystemNode::isDrive(const Common::String &path) const {
 	Common::String normalizedPath = Common::normalizePath(path, '/');
-	DrivesArray::const_iterator drive = Common::find(_config.drives.begin(), _config.drives.end(), normalizedPath);
-	return drive != _config.drives.end();
+	return _config.isDrive(normalizedPath);
 }
 
-
 #endif //#if defined(POSIX)
diff --git a/backends/fs/posix-drives/posix-drives-fs.h b/backends/fs/posix-drives/posix-drives-fs.h
index 5af57c66088..a94fca89cbb 100644
--- a/backends/fs/posix-drives/posix-drives-fs.h
+++ b/backends/fs/posix-drives/posix-drives-fs.h
@@ -40,8 +40,6 @@ protected:
 	}
 
 public:
-	typedef Common::Array<Common::String> DrivesArray;
-
 	enum BufferingMode {
 		/** IO buffering is fully disabled */
 		kBufferingModeDisabled,
@@ -52,11 +50,15 @@ public:
 	};
 
 	struct Config {
-		DrivesArray drives;
+		// Use the default stdio buffer size
+		Config() : bufferingMode(kBufferingModeStdio), bufferSize(0) { }
+		virtual ~Config() { }
+
+		virtual bool getDrives(AbstractFSList &list, bool hidden) const = 0;
+		virtual bool isDrive(const Common::String &path) const = 0;
+
 		BufferingMode bufferingMode;
 		uint32 bufferSize;
-
-		Config();
 	};
 
 	DrivePOSIXFilesystemNode(const Common::String &path, const Config &config);


Commit: e17b34c9dc6e7d25936ac1bbd6a4ff42d0ea85ef
    https://github.com/scummvm/scummvm/commit/e17b34c9dc6e7d25936ac1bbd6a4ff42d0ea85ef
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2023-01-20T14:14:03+01:00

Commit Message:
ANDROID: Store JNI environment in a thread local variable

This avoids to query JVM every time we need to do a JNI call.
A different environment is attached to each thread, hence the TLS
variable.

Changed paths:
    backends/platform/android/asset-archive.cpp
    backends/platform/android/jni-android.cpp
    backends/platform/android/jni-android.h


diff --git a/backends/platform/android/asset-archive.cpp b/backends/platform/android/asset-archive.cpp
index adc42e4ceb4..52514c76fbe 100644
--- a/backends/platform/android/asset-archive.cpp
+++ b/backends/platform/android/asset-archive.cpp
@@ -24,6 +24,12 @@
 #include <sys/types.h>
 #include <unistd.h>
 
+#include <android/asset_manager.h>
+#include <android/asset_manager_jni.h>
+
+#include "backends/platform/android/jni-android.h"
+#include "backends/platform/android/asset-archive.h"
+
 #include "common/str.h"
 #include "common/stream.h"
 #include "common/util.h"
@@ -31,12 +37,6 @@
 #include "common/debug.h"
 #include "common/textconsole.h"
 
-#include "backends/platform/android/jni-android.h"
-#include "backends/platform/android/asset-archive.h"
-
-#include <android/asset_manager.h>
-#include <android/asset_manager_jni.h>
-
 class AssetInputStream : public Common::SeekableReadStream {
 public:
 	AssetInputStream(AAssetManager *as, const Common::String &path);
diff --git a/backends/platform/android/jni-android.cpp b/backends/platform/android/jni-android.cpp
index de6472deda5..9d8854e2419 100644
--- a/backends/platform/android/jni-android.cpp
+++ b/backends/platform/android/jni-android.cpp
@@ -58,6 +58,8 @@ jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
 	return JNI::onLoad(vm);
 }
 
+pthread_key_t JNI::_env_tls;
+
 JavaVM *JNI::_vm = 0;
 jobject JNI::_jobj = 0;
 jobject JNI::_jobj_audio_track = 0;
@@ -141,6 +143,10 @@ JNI::~JNI() {
 }
 
 jint JNI::onLoad(JavaVM *vm) {
+	if (pthread_key_create(&_env_tls, NULL)) {
+		return JNI_ERR;
+	}
+
 	_vm = vm;
 
 	JNIEnv *env;
@@ -148,6 +154,10 @@ jint JNI::onLoad(JavaVM *vm) {
 	if (_vm->GetEnv((void **)&env, JNI_VERSION_1_2))
 		return JNI_ERR;
 
+	if (pthread_setspecific(_env_tls, env)) {
+		return JNI_ERR;
+	}
+
 	jclass cls = env->FindClass("org/scummvm/scummvm/ScummVM");
 	if (cls == 0)
 		return JNI_ERR;
@@ -158,8 +168,8 @@ jint JNI::onLoad(JavaVM *vm) {
 	return JNI_VERSION_1_2;
 }
 
-JNIEnv *JNI::getEnv() {
-	JNIEnv *env = 0;
+JNIEnv *JNI::fetchEnv() {
+	JNIEnv *env;
 
 	jint res = _vm->GetEnv((void **)&env, JNI_VERSION_1_2);
 
@@ -168,6 +178,8 @@ JNIEnv *JNI::getEnv() {
 		abort();
 	}
 
+	pthread_setspecific(_env_tls, env);
+
 	return env;
 }
 
@@ -180,9 +192,16 @@ void JNI::attachThread() {
 		LOGE("AttachCurrentThread() failed: %d", res);
 		abort();
 	}
+
+	if (pthread_setspecific(_env_tls, env)) {
+		LOGE("pthread_setspecific() failed");
+		abort();
+	}
 }
 
 void JNI::detachThread() {
+	pthread_setspecific(_env_tls, NULL);
+
 	jint res = _vm->DetachCurrentThread();
 
 	if (res != JNI_OK) {
diff --git a/backends/platform/android/jni-android.h b/backends/platform/android/jni-android.h
index 7b69e2a4211..b00ceff8aba 100644
--- a/backends/platform/android/jni-android.h
+++ b/backends/platform/android/jni-android.h
@@ -26,6 +26,7 @@
 
 #include <jni.h>
 #include <semaphore.h>
+#include <pthread.h>
 
 #include "common/fs.h"
 #include "common/archive.h"
@@ -59,7 +60,14 @@ public:
 
 	static jint onLoad(JavaVM *vm);
 
-	static JNIEnv *getEnv();
+	static inline JNIEnv *getEnv() {
+		JNIEnv *env = (JNIEnv*) pthread_getspecific(_env_tls);
+		if (env != nullptr) {
+			return env;
+		}
+
+		return fetchEnv();
+	}
 
 	static void attachThread();
 	static void detachThread();
@@ -102,6 +110,8 @@ public:
 	static bool isDirectoryWritableWithSAF(const Common::String &dirPath);
 
 private:
+	static pthread_key_t _env_tls;
+
 	static JavaVM *_vm;
 	// back pointer to (java) peer instance
 	static jobject _jobj;
@@ -172,6 +182,8 @@ private:
 	static Common::U32String convertFromJString(JNIEnv *env, const jstring &jstr);
 
 	static PauseToken _pauseToken;
+
+	static JNIEnv *fetchEnv();
 };
 
 inline bool JNI::haveSurface() {


Commit: c84c9cd3139c2ffbf79d55bb41ad275ef0507d88
    https://github.com/scummvm/scummvm/commit/c84c9cd3139c2ffbf79d55bb41ad275ef0507d88
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2023-01-20T14:14:03+01:00

Commit Message:
ANDROID: Update build tools

Update minSdkVersion to fit with what the code do

Changed paths:
    dists/android/AndroidManifest.xml
    dists/android/build.gradle
    dists/android/gradle/wrapper/gradle-wrapper.jar
    dists/android/gradle/wrapper/gradle-wrapper.properties


diff --git a/dists/android/AndroidManifest.xml b/dists/android/AndroidManifest.xml
index bb97bcae376..5f9a99af9ce 100644
--- a/dists/android/AndroidManifest.xml
+++ b/dists/android/AndroidManifest.xml
@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-	package="org.scummvm.scummvm"
 	android:installLocation="auto"
 	android:launchMode="singleTask"
 	android:sharedUserId="org.scummvm.scummvm">
@@ -43,6 +42,7 @@
 		android:requestLegacyExternalStorage="true">
 		<activity
 			android:name=".SplashActivity"
+			android:exported="true"
 			android:banner="@drawable/leanback_icon"
 			android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
 			android:screenOrientation="landscape"
@@ -58,14 +58,12 @@
 		</activity>
 		<activity
 			android:name=".ScummVMActivity"
+			android:exported="false"
 			android:banner="@drawable/leanback_icon"
 			android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
 			android:screenOrientation="landscape"
 			android:theme="@style/AppTheme"
 			android:windowSoftInputMode="adjustResize">
-			<intent-filter>
-				<category android:name="tv.ouya.intent.category.GAME" />
-			</intent-filter>
 		</activity>
 	</application>
 
diff --git a/dists/android/build.gradle b/dists/android/build.gradle
index 3006f223e4d..4771c0ffab7 100644
--- a/dists/android/build.gradle
+++ b/dists/android/build.gradle
@@ -1,38 +1,41 @@
 buildscript {
     repositories {
+        gradlePluginPortal()
         google()
-        jcenter()
+        mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:4.1.1'
+        classpath 'com.android.tools.build:gradle:7.3.1'
     }
 }
 
 dependencies {
     repositories {
         google()
-        jcenter()
+        mavenCentral()
     }
 }
 
 // Enable to see use of deprecated API
- tasks.withType(JavaCompile) {
+tasks.withType(JavaCompile) {
 	 options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked"
- }
+}
 
 apply plugin: 'com.android.application'
 
 android {
     compileSdkVersion 29
-    buildToolsVersion "29.0.3"
+    buildToolsVersion "33.0.1"
     ndkVersion "21.3.6528147"
 
+    namespace "org.scummvm.scummvm"
+
     defaultConfig {
         applicationId "org.scummvm.scummvm"
 
         setProperty("archivesBaseName", "ScummVM")
 
-        minSdkVersion 16
+        minSdkVersion 19
         targetSdkVersion 29
 
         versionName "2.7.0"
diff --git a/dists/android/gradle/wrapper/gradle-wrapper.jar b/dists/android/gradle/wrapper/gradle-wrapper.jar
index f6b961fd5a8..943f0cbfa75 100644
Binary files a/dists/android/gradle/wrapper/gradle-wrapper.jar and b/dists/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/dists/android/gradle/wrapper/gradle-wrapper.properties b/dists/android/gradle/wrapper/gradle-wrapper.properties
index 3c9d0852bfa..f398c33c4b0 100644
--- a/dists/android/gradle/wrapper/gradle-wrapper.properties
+++ b/dists/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
+networkTimeout=10000
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip


Commit: 96315679238a5d252c481c7ebd5a8f49b95efa27
    https://github.com/scummvm/scummvm/commit/96315679238a5d252c481c7ebd5a8f49b95efa27
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2023-01-20T14:14:03+01:00

Commit Message:
ANDROID: Add a method to get running SDK version

Co-authored-by: antoniou79 <a.antoniou79 at gmail.com>

Changed paths:
    backends/platform/android/android.cpp
    backends/platform/android/jni-android.cpp
    backends/platform/android/jni-android.h


diff --git a/backends/platform/android/android.cpp b/backends/platform/android/android.cpp
index e014520f824..ca7c3fce8e9 100644
--- a/backends/platform/android/android.cpp
+++ b/backends/platform/android/android.cpp
@@ -154,6 +154,12 @@ OSystem_Android::OSystem_Android(int audio_sample_rate, int audio_buffer_size) :
 			getSystemProperty("ro.build.display.id").c_str(),
 			getSystemProperty("ro.build.version.sdk").c_str(),
 			getSystemProperty("ro.product.cpu.abi").c_str());
+	// JNI::getAndroidSDKVersionId() should be identical to the result from ("ro.build.version.sdk"),
+	// though getting it via JNI is maybe the most reliable option (?)
+	// Also __system_property_get which is used by getSystemProperty() is being deprecated in recent NDKs
+
+	int sdkVersion = JNI::getAndroidSDKVersionId();
+	LOGI("SDK Version: %d", sdkVersion);
 }
 
 OSystem_Android::~OSystem_Android() {
diff --git a/backends/platform/android/jni-android.cpp b/backends/platform/android/jni-android.cpp
index 9d8854e2419..29d67cc0df9 100644
--- a/backends/platform/android/jni-android.cpp
+++ b/backends/platform/android/jni-android.cpp
@@ -872,11 +872,30 @@ void JNI::setPause(JNIEnv *env, jobject self, jboolean value) {
 	}
 }
 
-
 jstring JNI::getNativeVersionInfo(JNIEnv *env, jobject self) {
 	return convertToJString(env, Common::U32String(gScummVMVersion));
 }
 
+jint JNI::getAndroidSDKVersionId() {
+	// based on: https://stackoverflow.com/a/10511880
+	JNIEnv *env = JNI::getEnv();
+	// VERSION is a nested class within android.os.Build (hence "$" rather than "/")
+	jclass versionClass = env->FindClass("android/os/Build$VERSION");
+	if (!versionClass) {
+		return 0;
+	}
+
+	jfieldID sdkIntFieldID = NULL;
+	sdkIntFieldID = env->GetStaticFieldID(versionClass, "SDK_INT", "I");
+	if (!sdkIntFieldID) {
+		return 0;
+	}
+
+	jint sdkInt = env->GetStaticIntField(versionClass, sdkIntFieldID);
+	//LOGD("sdkInt = %d", sdkInt);
+	return sdkInt;
+}
+
 jstring JNI::convertToJString(JNIEnv *env, const Common::U32String &str) {
 	uint len = 0;
 	uint16 *u16str = str.encodeUTF16Native(&len);
diff --git a/backends/platform/android/jni-android.h b/backends/platform/android/jni-android.h
index b00ceff8aba..2942e754fd4 100644
--- a/backends/platform/android/jni-android.h
+++ b/backends/platform/android/jni-android.h
@@ -89,6 +89,7 @@ public:
 	static int getTouchMode();
 	static void showSAFRevokePermsControl(bool enable);
 	static void addSysArchivesToSearchSet(Common::SearchSet &s, int priority);
+	static jint getAndroidSDKVersionId();
 
 	static inline bool haveSurface();
 	static inline bool swapBuffers();


Commit: c60ad0a554413b930f4403c1d4660820dfd9c2f9
    https://github.com/scummvm/scummvm/commit/c60ad0a554413b930f4403c1d4660820dfd9c2f9
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2023-01-20T14:14:03+01:00

Commit Message:
ANDROID: Various cleanups in JNI

Mostly fixing memory leaks (Java references).
No need to preinit semaphore (sem_init is called).
First initialize methods ID before starting anything.
Aborts if a method is not found.

Changed paths:
    backends/platform/android/jni-android.cpp


diff --git a/backends/platform/android/jni-android.cpp b/backends/platform/android/jni-android.cpp
index 29d67cc0df9..d04e6657348 100644
--- a/backends/platform/android/jni-android.cpp
+++ b/backends/platform/android/jni-android.cpp
@@ -71,7 +71,7 @@ Common::Archive *JNI::_asset_archive = 0;
 OSystem_Android *JNI::_system = 0;
 
 bool JNI::pause = false;
-sem_t JNI::pause_sem = { 0 };
+sem_t JNI::pause_sem;
 
 int JNI::surface_changeid = 0;
 int JNI::egl_surface_width = 0;
@@ -165,6 +165,7 @@ jint JNI::onLoad(JavaVM *vm) {
 	if (env->RegisterNatives(cls, _natives, ARRAYSIZE(_natives)) < 0)
 		return JNI_ERR;
 
+	env->DeleteLocalRef(cls);
 	return JNI_VERSION_1_2;
 }
 
@@ -550,6 +551,7 @@ void JNI::addSysArchivesToSearchSet(Common::SearchSet &s, int priority) {
 
 		env->DeleteLocalRef(path_obj);
 	}
+	env->DeleteLocalRef(array);
 
 	// add the internal asset (android's structure) with a lower priority,
 	// since:
@@ -576,6 +578,7 @@ bool JNI::initSurface() {
 	}
 
 	_jobj_egl_surface = env->NewGlobalRef(obj);
+	env->DeleteLocalRef(obj);
 
 	return true;
 }
@@ -583,6 +586,9 @@ bool JNI::initSurface() {
 void JNI::deinitSurface() {
 	JNIEnv *env = JNI::getEnv();
 
+	env->DeleteGlobalRef(_jobj_egl_surface);
+	_jobj_egl_surface = 0;
+
 	env->CallVoidMethod(_jobj, _MID_deinitSurface);
 
 	if (env->ExceptionCheck()) {
@@ -591,9 +597,6 @@ void JNI::deinitSurface() {
 		env->ExceptionDescribe();
 		env->ExceptionClear();
 	}
-
-	env->DeleteGlobalRef(_jobj_egl_surface);
-	_jobj_egl_surface = 0;
 }
 
 void JNI::setAudioPause() {
@@ -649,19 +652,11 @@ void JNI::setAudioStop() {
 void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
 				jobject egl, jobject egl_display,
 				jobject at, jint audio_sample_rate, jint audio_buffer_size) {
-	LOGI("%s", gScummVMFullVersion);
+	LOGI("Native version: %s", gScummVMFullVersion);
 
 	assert(!_system);
 
-	pause = false;
-	// initial value of zero!
-	sem_init(&pause_sem, 0, 0);
-
-	_asset_archive = new AndroidAssetArchive(asset_manager);
-	assert(_asset_archive);
-
-	_system = new OSystem_Android(audio_sample_rate, audio_buffer_size);
-	assert(_system);
+	// Resolve every JNI method before anything else in case we need it
 
 	// weak global ref to allow class to be unloaded
 	// ... except dalvik implements NewWeakGlobalRef only on froyo
@@ -671,11 +666,13 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
 
 	jclass cls = env->GetObjectClass(_jobj);
 
-#define FIND_METHOD(prefix, name, signature) do {							\
-		_MID_ ## prefix ## name = env->GetMethodID(cls, #name, signature);	\
-		if (_MID_ ## prefix ## name == 0)									\
-			return;															\
-	} 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 function %s", #name);                          \
+            abort();                                                        \
+        }                                                                   \
+    } while (0)
 
 	FIND_METHOD(, setWindowCaption, "(Ljava/lang/String;)V");
 	FIND_METHOD(, getDPI, "([F)V");
@@ -703,6 +700,8 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
 	_jobj_egl = env->NewGlobalRef(egl);
 	_jobj_egl_display = env->NewGlobalRef(egl_display);
 
+	env->DeleteLocalRef(cls);
+
 	cls = env->GetObjectClass(_jobj_egl);
 
 	FIND_METHOD(EGL10_, eglSwapBuffers,
@@ -711,6 +710,8 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
 
 	_jobj_audio_track = env->NewGlobalRef(at);
 
+	env->DeleteLocalRef(cls);
+
 	cls = env->GetObjectClass(_jobj_audio_track);
 
 	FIND_METHOD(AudioTrack_, flush, "()V");
@@ -719,8 +720,19 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
 	FIND_METHOD(AudioTrack_, stop, "()V");
 	FIND_METHOD(AudioTrack_, write, "([BII)I");
 
+	env->DeleteLocalRef(cls);
 #undef FIND_METHOD
 
+	pause = false;
+	// initial value of zero!
+	sem_init(&pause_sem, 0, 0);
+
+	_asset_archive = new AndroidAssetArchive(asset_manager);
+	assert(_asset_archive);
+
+	_system = new OSystem_Android(audio_sample_rate, audio_buffer_size);
+	assert(_system);
+
 	g_system = _system;
 }
 
@@ -893,6 +905,8 @@ jint JNI::getAndroidSDKVersionId() {
 
 	jint sdkInt = env->GetStaticIntField(versionClass, sdkIntFieldID);
 	//LOGD("sdkInt = %d", sdkInt);
+
+	env->DeleteLocalRef(versionClass);
 	return sdkInt;
 }
 
@@ -917,7 +931,7 @@ Common::U32String JNI::convertFromJString(JNIEnv *env, const jstring &jstr) {
 
 // TODO should this be a U32String array?
 Common::Array<Common::String> JNI::getAllStorageLocations() {
-	Common::Array<Common::String> *res = new Common::Array<Common::String>();
+	Common::Array<Common::String> res;
 
 	JNIEnv *env = JNI::getEnv();
 
@@ -930,7 +944,7 @@ Common::Array<Common::String> JNI::getAllStorageLocations() {
 		env->ExceptionDescribe();
 		env->ExceptionClear();
 
-		return *res;
+		return res;
 	}
 
 	jsize size = env->GetArrayLength(array);
@@ -939,14 +953,15 @@ Common::Array<Common::String> JNI::getAllStorageLocations() {
 		const char *path = env->GetStringUTFChars(path_obj, 0);
 
 		if (path != 0) {
-			res->push_back(path);
+			res.push_back(path);
 			env->ReleaseStringUTFChars(path_obj, path);
 		}
 
 		env->DeleteLocalRef(path_obj);
 	}
 
-	return *res;
+	env->DeleteLocalRef(array);
+	return res;
 }
 
 bool JNI::createDirectoryWithSAF(const Common::String &dirPath) {
@@ -1018,6 +1033,4 @@ bool JNI::isDirectoryWritableWithSAF(const Common::String &dirPath) {
 
 	return isWritable;
 }
-
 #endif
-


Commit: 0fdf53f984add551b4dc2b9aae2e13ddf878a5d7
    https://github.com/scummvm/scummvm/commit/0fdf53f984add551b4dc2b9aae2e13ddf878a5d7
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2023-01-20T14:14:03+01:00

Commit Message:
ANDROID: Remove platform specific code from shared code

Changed paths:
    backends/fs/posix/posix-fs.cpp
    backends/fs/posix/posix-iostream.cpp
    backends/fs/posix/posix-iostream.h
    gui/browser.cpp


diff --git a/backends/fs/posix/posix-fs.cpp b/backends/fs/posix/posix-fs.cpp
index dae604bcf89..4db92eb7e96 100644
--- a/backends/fs/posix/posix-fs.cpp
+++ b/backends/fs/posix/posix-fs.cpp
@@ -55,10 +55,6 @@
 #include <os2.h>
 #endif
 
-#if defined(ANDROID_PLAIN_PORT)
-#include "backends/platform/android/jni-android.h"
-#endif
-
 bool POSIXFilesystemNode::exists() const {
 	return access(_path.c_str(), F_OK) == 0;
 }
@@ -68,14 +64,7 @@ bool POSIXFilesystemNode::isReadable() const {
 }
 
 bool POSIXFilesystemNode::isWritable() const {
-	bool retVal = access(_path.c_str(), W_OK) == 0;
-#if defined(ANDROID_PLAIN_PORT)
-	if (!retVal) {
-		// Update return value if going through Android's SAF grants the permission
-		retVal = JNI::isDirectoryWritableWithSAF(_path);
-	}
-#endif // ANDROID_PLAIN_PORT
-	return retVal;
+	return access(_path.c_str(), W_OK) == 0;
 }
 
 void POSIXFilesystemNode::setFlags() {
@@ -179,23 +168,6 @@ bool POSIXFilesystemNode::getChildren(AbstractFSList &myList, ListMode mode, boo
 	}
 #endif
 
-#if defined(ANDROID_PLAIN_PORT)
-	if (_path == "/") {
-		Common::Array<Common::String> list = JNI::getAllStorageLocations();
-		for (Common::Array<Common::String>::const_iterator it = list.begin(), end = list.end(); it != end; ++it) {
-			POSIXFilesystemNode *entry = new POSIXFilesystemNode();
-
-			entry->_isDirectory = true;
-			entry->_isValid = true;
-			entry->_displayName = *it;
-			++it;
-			entry->_path = *it;
-			myList.push_back(entry);
-		}
-		return true;
-	}
-#endif
-
 	DIR *dirp = opendir(_path.c_str());
 	struct dirent *dp;
 
@@ -272,12 +244,6 @@ AbstractFSNode *POSIXFilesystemNode::getParent() const {
 	if (_path.size() == 3 && _path.hasSuffix(":/"))
 		// This is a root directory of a drive
 		return makeNode("/");   // return a virtual root for a list of drives
-#elif defined(ANDROID_PLAIN_PORT)
-	Common::String pathCopy = _path;
-	pathCopy.trim();
-	if (pathCopy.empty()) {
-		return makeNode("/");   // return a virtual root for a list of drives
-	}
 #endif
 
 	const char *start = _path.c_str();
@@ -310,17 +276,6 @@ Common::SeekableWriteStream *POSIXFilesystemNode::createWriteStream() {
 bool POSIXFilesystemNode::createDirectory() {
 	if (mkdir(_path.c_str(), 0755) == 0)
 		setFlags();
-#if defined(ANDROID_PLAIN_PORT)
-	else {
-		// TODO eventually android specific stuff should be moved to an Android backend for fs
-		//      peterkohaut already has some work on that in his fork (moving the port to more native code)
-		//      However, I have not found a way to do this Storage Access Framework stuff natively yet.
-		if (JNI::createDirectoryWithSAF(_path)) {
-			setFlags();
-		}
-	}
-#endif // ANDROID_PLAIN_PORT
-
 
 	return _isValid && _isDirectory;
 }
diff --git a/backends/fs/posix/posix-iostream.cpp b/backends/fs/posix/posix-iostream.cpp
index 4a97577be7b..41f625d8a86 100644
--- a/backends/fs/posix/posix-iostream.cpp
+++ b/backends/fs/posix/posix-iostream.cpp
@@ -25,12 +25,6 @@
 
 #include <sys/stat.h>
 
-#if defined(ANDROID_PLAIN_PORT)
-#include "backends/platform/android/jni-android.h"
-#include <unistd.h>
-#endif
-
-
 PosixIoStream *PosixIoStream::makeFromPath(const Common::String &path, bool writeMode) {
 #if defined(HAS_FSEEKO64)
 	FILE *handle = fopen64(path.c_str(), writeMode ? "wb" : "rb");
@@ -41,72 +35,12 @@ PosixIoStream *PosixIoStream::makeFromPath(const Common::String &path, bool writ
 	if (handle)
 		return new PosixIoStream(handle);
 
-#if defined(ANDROID_PLAIN_PORT)
-	else {
-		// TODO also address case for writeMode false
-
-		// TODO eventually android specific stuff should be moved to an Android backend for fs
-		//      peterkohaut already has some work on that in his fork (moving the port to more native code)
-		//      However, I have not found a way to do this Storage Access Framework stuff natively yet.
-
-		// if we are here we are only interested in hackyFilenames -- which mean we went through SAF. Otherwise we ignore the case
-		if (writeMode) {
-			Common::String hackyFilename = JNI::createFileWithSAF(path);
-			// https://stackoverflow.com/questions/59000390/android-accessing-files-in-native-c-c-code-with-google-scoped-storage-api
-			//warning ("PosixIoStream::makeFromPath() JNI::createFileWithSAF returned: %s", hackyFilename.c_str() );
-			if (strstr(hackyFilename.c_str(), "/proc/self/fd/") == hackyFilename.c_str()) {
-				//warning ("PosixIoStream::makeFromPath() match with hacky prefix!" );
-				int fd = atoi(hackyFilename.c_str() + 14);
-				if (fd != 0) {
-					//warning ("PosixIoStream::makeFromPath() got fd int: %d!", fd );
-					// Why dup(fd) below: if we called fdopen() on the
-					// original fd value, and the native code closes
-					// and tries to re-open that file, the second fdopen(fd)
-					// would fail, return NULL - after closing the
-					// original fd received from Android, it's no longer valid.
-					FILE *safHandle = fdopen(dup(fd), "wb");
-					// Why rewind(fp): if the native code closes and
-					// opens again the file, the file read/write position
-					// would not change, because with dup(fd) it's still
-					// the same file...
-					rewind(safHandle);
-					if (safHandle) {
-						return new PosixIoStream(safHandle, true, hackyFilename);
-					}
-				}
-			}
-	   }
-	}
-#endif // ANDROID_PLAIN_PORT
-
 	return nullptr;
 }
 
 
-#if defined(ANDROID_PLAIN_PORT)
-PosixIoStream::PosixIoStream(void *handle, bool bCreatedWithSAF, Common::String sHackyFilename) :
-		StdioStream(handle) {
-	createdWithSAF = bCreatedWithSAF;
-	hackyfilename = sHackyFilename;
-}
-
-PosixIoStream::~PosixIoStream() {
-	//warning("PosixIoStream::~PosixIoStream() closing file");
-	if (createdWithSAF && !hackyfilename.empty() ) {
-		JNI::closeFileWithSAF(hackyfilename);
-	}
-	// we'leave the base class destructor to close the FILE
-	// it does not seem to matter that the operation is done
-	// after the JNI call to close the descriptor on the Java side
-}
-#endif // ANDROID_PLAIN_PORT
-
 PosixIoStream::PosixIoStream(void *handle) :
 		StdioStream(handle) {
-#if defined(ANDROID_PLAIN_PORT)
-	createdWithSAF = false;
-	hackyfilename = "";
-#endif // ANDROID_PLAIN_PORT
 }
 
 int64 PosixIoStream::size() const {
diff --git a/backends/fs/posix/posix-iostream.h b/backends/fs/posix/posix-iostream.h
index cb39f280311..741910d8c67 100644
--- a/backends/fs/posix/posix-iostream.h
+++ b/backends/fs/posix/posix-iostream.h
@@ -29,17 +29,8 @@
  */
 class PosixIoStream final : public StdioStream {
 public:
-#if defined(ANDROID_PLAIN_PORT)
-	bool createdWithSAF;
-	Common::String hackyfilename;
-#endif
-
 	static PosixIoStream *makeFromPath(const Common::String &path, bool writeMode);
 	PosixIoStream(void *handle);
-#if defined(ANDROID_PLAIN_PORT)
-	PosixIoStream(void *handle, bool bCreatedWithSAF, Common::String sHackyFilename);
-	~PosixIoStream() override;
-#endif
 
 	int64 size() const override;
 };
diff --git a/gui/browser.cpp b/gui/browser.cpp
index 4ec411f9a76..fd90546a4b6 100644
--- a/gui/browser.cpp
+++ b/gui/browser.cpp
@@ -103,22 +103,8 @@ void BrowserDialog::open() {
 	// Call super implementation
 	Dialog::open();
 
-#if defined(ANDROID_PLAIN_PORT)
-	// Currently, the "default" path in Android port will present a list of shortcuts, (most of) which should be usable.
-	// The "/" will list these shortcuts (see POSIXFilesystemNode::getChildren())
-	Common::String blPath = "/";
-	if (ConfMan.hasKey("browser_lastpath")) {
-		Common::String blPathCandidate = ConfMan.get("browser_lastpath");
-		blPathCandidate.trim();
-		if (!blPathCandidate.empty()) {
-			blPath = blPathCandidate;
-		}
-	}
-	_node = Common::FSNode(blPath);
-#else
 	if (ConfMan.hasKey("browser_lastpath"))
 		_node = Common::FSNode(ConfMan.get("browser_lastpath"));
-#endif
 
 	if (!_node.isDirectory())
 		_node = Common::FSNode(".");
@@ -133,23 +119,7 @@ void BrowserDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data
 	switch (cmd) {
 	//Search for typed-in directory
 	case kPathEditedCmd:
-#if defined(ANDROID_PLAIN_PORT)
-	{
-		// Currently, the "default" path in Android port will present a list of shortcuts, (most of) which should be usable.
-		// The "/" will list these shortcuts (see POSIXFilesystemNode::getChildren())
-		// If the user enters an empty text or blank spaces for the path, then upon committing it as an edit,
-		// Android will show the list of shortcuts and default the path text field to "/".
-		// The code is placed in brackets for edtPath var to have proper local scope in this particular switch case.
-		Common::String edtPath = Common::convertFromU32String(_currentPath->getEditString());
-		edtPath.trim();
-		if (edtPath.empty()) {
-			edtPath = "/";
-		}
-		_node = Common::FSNode(edtPath);
-	}
-#else
 		_node = Common::FSNode(Common::convertFromU32String(_currentPath->getEditString()));
-#endif
 		updateListing();
 		break;
 	//Search by text input


Commit: ca1dbfc9d6548ba727929545dc5c783f4a398328
    https://github.com/scummvm/scummvm/commit/ca1dbfc9d6548ba727929545dc5c783f4a398328
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2023-01-20T14:14:03+01:00

Commit Message:
ANDROID: Add SAF support

Changed paths:
  A backends/fs/android/android-fs-factory.cpp
  A backends/fs/android/android-fs-factory.h
  A backends/fs/android/android-posix-fs.cpp
  A backends/fs/android/android-posix-fs.h
  A backends/fs/android/android-saf-fs.cpp
  A backends/fs/android/android-saf-fs.h
  A backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
    backends/module.mk
    backends/platform/android/android.cpp
    backends/platform/android/jni-android.cpp
    backends/platform/android/jni-android.h
    backends/platform/android/org/scummvm/scummvm/ScummVM.java
    backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
    dists/android/build.gradle


diff --git a/backends/fs/android/android-fs-factory.cpp b/backends/fs/android/android-fs-factory.cpp
new file mode 100644
index 00000000000..538d415c501
--- /dev/null
+++ b/backends/fs/android/android-fs-factory.cpp
@@ -0,0 +1,123 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#if defined(__ANDROID__)
+
+#include "backends/platform/android/jni-android.h"
+
+#include "backends/fs/android/android-fs-factory.h"
+#include "backends/fs/android/android-posix-fs.h"
+#include "backends/fs/android/android-saf-fs.h"
+
+namespace Common {
+DECLARE_SINGLETON(AndroidFilesystemFactory);
+}
+
+AndroidFilesystemFactory::AndroidFilesystemFactory() : _withSAF(false), _config(this) {
+}
+
+void AndroidFilesystemFactory::initSAF() {
+	_withSAF = true;
+	AndroidSAFFilesystemNode::initJNI();
+}
+
+AbstractFSNode *AndroidFilesystemFactory::makeRootFileNode() const {
+	return new AndroidPOSIXFilesystemNode(_config);
+}
+
+AbstractFSNode *AndroidFilesystemFactory::makeCurrentDirectoryFileNode() const {
+	// As current working directory can point outside of our data don't take any risk
+	return makeRootFileNode();
+}
+
+AbstractFSNode *AndroidFilesystemFactory::makeFileNodePath(const Common::String &path) const {
+	if (path.empty() || path.equals("/")) {
+		return makeRootFileNode();
+	}
+
+	// No need to take SAF add mode here as it's called only for paths and we won't accept /saf path to make a new SAF
+
+	// If SAF works, it was a SAF URL
+	if (_withSAF) {
+		AbstractFSNode *node = AndroidSAFFilesystemNode::makeFromPath(path);
+		if (node) {
+			return node;
+		}
+	}
+
+	return new AndroidPOSIXFilesystemNode(path, _config);
+}
+
+AndroidFilesystemFactory::Config::Config(const AndroidFilesystemFactory *factory) : _factory(factory),
+	_storages(JNI::getAllStorageLocations()) {
+}
+
+bool AndroidFilesystemFactory::Config::getDrives(AbstractFSList &list, bool hidden) const {
+	Common::Array<jobject> trees;
+	if (_factory->_withSAF) {
+		trees = JNI::getSAFTrees();
+	}
+
+	list.reserve(trees.size() + _storages.size() / 2);
+
+	// For SAF
+	if (_factory->_withSAF) {
+		list.push_back(new AddSAFFakeNode());
+	}
+
+	// If _withSAF is false, trees will be empty
+	for (Common::Array<jobject>::iterator it = trees.begin(); it != trees.end(); it++) {
+		AbstractFSNode *node = AndroidSAFFilesystemNode::makeFromTree(*it);
+		if (!node) {
+			continue;
+		}
+
+		list.push_back(node);
+	}
+
+	// For old POSIX way
+	for (Common::Array<Common::String>::const_iterator it = _storages.begin(); it != _storages.end(); ++it) {
+		const Common::String &driveName = *it;
+		++it;
+		const Common::String &drivePath = *it;
+
+		AndroidPOSIXFilesystemNode *node = new AndroidPOSIXFilesystemNode(drivePath, *this);
+		node->_displayName = driveName;
+
+		list.push_back(node);
+	}
+	return true;
+}
+
+bool AndroidFilesystemFactory::Config::isDrive(const Common::String &path) const {
+	// This function is called from DrivePOSIXFilesystemNode::isDrive
+	// DrivePOSIXFilesystemNode is only used for POSIX code so no need to look for SAF
+
+	for (Common::Array<Common::String>::const_iterator it = _storages.begin(); it != _storages.end(); it++) {
+		++it;
+		if (*it == path) {
+			return true;
+		}
+	}
+	return false;
+}
+
+#endif
diff --git a/backends/fs/android/android-fs-factory.h b/backends/fs/android/android-fs-factory.h
new file mode 100644
index 00000000000..127f342163d
--- /dev/null
+++ b/backends/fs/android/android-fs-factory.h
@@ -0,0 +1,61 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef ANDROID_FILESYSTEM_FACTORY_H
+#define ANDROID_FILESYSTEM_FACTORY_H
+
+#include "backends/fs/fs-factory.h"
+#include "common/singleton.h"
+#include "backends/fs/posix-drives/posix-drives-fs.h"
+
+/**
+ * Creates AndroidFilesystemNode objects.
+ *
+ * Parts of this class are documented in the base interface class, FilesystemFactory.
+ */
+class AndroidFilesystemFactory final : public FilesystemFactory,
+	public Common::Singleton<AndroidFilesystemFactory> {
+	friend class Common::Singleton<SingletonBaseType>;
+protected:
+	AndroidFilesystemFactory();
+public:
+	void initSAF();
+	AbstractFSNode *makeRootFileNode() const override;
+	AbstractFSNode *makeCurrentDirectoryFileNode() const override;
+	AbstractFSNode *makeFileNodePath(const Common::String &path) const override;
+
+private:
+	struct Config : public DrivePOSIXFilesystemNode::Config {
+		Config(const AndroidFilesystemFactory *factory);
+
+		bool getDrives(AbstractFSList &list, bool hidden) const override;
+		bool isDrive(const Common::String &path) const override;
+
+	private:
+		const AndroidFilesystemFactory *_factory;
+		Common::Array<Common::String> _storages;
+	};
+
+	bool _withSAF;
+	Config _config;
+};
+
+#endif /*ANDROID_FILESYSTEM_FACTORY_H*/
diff --git a/backends/fs/android/android-posix-fs.cpp b/backends/fs/android/android-posix-fs.cpp
new file mode 100644
index 00000000000..f607c767d2b
--- /dev/null
+++ b/backends/fs/android/android-posix-fs.cpp
@@ -0,0 +1,42 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#if defined(__ANDROID__)
+
+#include "backends/fs/android/android-fs-factory.h"
+#include "backends/fs/android/android-posix-fs.h"
+#include "backends/fs/android/android-saf-fs.h"
+
+AbstractFSNode *AndroidPOSIXFilesystemNode::makeNode() const {
+	return new AndroidPOSIXFilesystemNode(_config);
+}
+
+AbstractFSNode *AndroidPOSIXFilesystemNode::makeNode(const Common::String &path) const {
+	// If SAF works, it was a SAF URL
+	AbstractFSNode *node = AndroidSAFFilesystemNode::makeFromPath(path);
+	if (node) {
+		return node;
+	}
+
+	return new AndroidPOSIXFilesystemNode(path, _config);
+}
+
+#endif
diff --git a/backends/fs/android/android-posix-fs.h b/backends/fs/android/android-posix-fs.h
new file mode 100644
index 00000000000..c4305778e9e
--- /dev/null
+++ b/backends/fs/android/android-posix-fs.h
@@ -0,0 +1,41 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef ANDROID_FILESYSTEM_ROOT_H
+#define ANDROID_FILESYSTEM_ROOT_H
+
+#include "backends/fs/posix-drives/posix-drives-fs.h"
+
+class AndroidPOSIXFilesystemNode : public DrivePOSIXFilesystemNode {
+	// To let the factory redefine the displayed name
+	friend class AndroidFilesystemFactory;
+protected:
+	AbstractFSNode *makeNode() const override;
+	AbstractFSNode *makeNode(const Common::String &path) const override;
+
+public:
+	AndroidPOSIXFilesystemNode(const Common::String &path, const Config &config)
+		: DrivePOSIXFilesystemNode(path, config) { }
+	AndroidPOSIXFilesystemNode(const Config &config)
+		: DrivePOSIXFilesystemNode(config) { _path = "/"; }
+};
+
+#endif
diff --git a/backends/fs/android/android-saf-fs.cpp b/backends/fs/android/android-saf-fs.cpp
new file mode 100644
index 00000000000..8d90cc9af3e
--- /dev/null
+++ b/backends/fs/android/android-saf-fs.cpp
@@ -0,0 +1,654 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#if defined(__ANDROID__)
+
+// Allow use of stuff in <time.h> and abort()
+#define FORBIDDEN_SYMBOL_EXCEPTION_time_h
+#define FORBIDDEN_SYMBOL_EXCEPTION_abort
+
+// Disable printf override in common/forbidden.h to avoid
+// clashes with log.h from the Android SDK.
+// That header file uses
+//   __attribute__ ((format(printf, 3, 4)))
+// which gets messed up by our override mechanism; this could
+// be avoided by either changing the Android SDK to use the equally
+// legal and valid
+//   __attribute__ ((format(printf, 3, 4)))
+// or by refining our printf override to use a varadic macro
+// (which then wouldn't be portable, though).
+// Anyway, for now we just disable the printf override globally
+// for the Android port
+#define FORBIDDEN_SYMBOL_EXCEPTION_printf
+
+// Allow calling of fdopen
+#define FORBIDDEN_SYMBOL_EXCEPTION_FILE
+
+// Allow calling of close system call
+#include <unistd.h>
+
+#include "backends/platform/android/android.h"
+#include "backends/platform/android/jni-android.h"
+
+#include "backends/fs/android/android-fs-factory.h"
+#include "backends/fs/android/android-saf-fs.h"
+
+#include "backends/fs/posix/posix-iostream.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+
+jmethodID AndroidSAFFilesystemNode::_MID_getTreeId = 0;
+jmethodID AndroidSAFFilesystemNode::_MID_pathToNode = 0;
+jmethodID AndroidSAFFilesystemNode::_MID_getChildren = 0;
+jmethodID AndroidSAFFilesystemNode::_MID_getChild = 0;
+jmethodID AndroidSAFFilesystemNode::_MID_createDirectory = 0;
+jmethodID AndroidSAFFilesystemNode::_MID_createFile = 0;
+jmethodID AndroidSAFFilesystemNode::_MID_createReadStream = 0;
+jmethodID AndroidSAFFilesystemNode::_MID_createWriteStream = 0;
+
+jfieldID AndroidSAFFilesystemNode::_FID__treeName = 0;
+jfieldID AndroidSAFFilesystemNode::_FID__root = 0;
+
+jfieldID AndroidSAFFilesystemNode::_FID__parent = 0;
+jfieldID AndroidSAFFilesystemNode::_FID__path = 0;
+jfieldID AndroidSAFFilesystemNode::_FID__documentId = 0;
+jfieldID AndroidSAFFilesystemNode::_FID__flags = 0;
+
+bool AndroidSAFFilesystemNode::_JNIinit = false;
+
+const char AndroidSAFFilesystemNode::SAF_MOUNT_POINT[] = "/saf/";
+
+void AndroidSAFFilesystemNode::initJNI() {
+	if (_JNIinit) {
+		return;
+	}
+
+	JNIEnv *env = JNI::getEnv();
+
+#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);                           \
+    } 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);                            \
+    } while (0)
+#define SAFFSNodeSig "Lorg/scummvm/scummvm/SAFFSTree$SAFFSNode;"
+
+	jclass cls = env->FindClass("org/scummvm/scummvm/SAFFSTree");
+
+	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_FIELD(, _treeName, "Ljava/lang/String;");
+	FIND_FIELD(, _root, SAFFSNodeSig);
+
+	cls = env->FindClass("org/scummvm/scummvm/SAFFSTree$SAFFSNode");
+
+	FIND_FIELD(, _parent, SAFFSNodeSig);
+	FIND_FIELD(, _path, "Ljava/lang/String;");
+	FIND_FIELD(, _documentId, "Ljava/lang/String;");
+	FIND_FIELD(, _flags, "I");
+
+#undef SAFFSNodeSig
+#undef FIND_FIELD
+#undef FIND_METHOD
+
+	_JNIinit = true;
+}
+
+AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromPath(const Common::String &path) {
+	if (!path.hasPrefix(SAF_MOUNT_POINT)) {
+		// Not a SAF mount point
+		return nullptr;
+	}
+
+	// Path is in the form /saf/<treeid>/<path>
+	size_t pos = path.findFirstOf('/', sizeof(SAF_MOUNT_POINT) - 1);
+	Common::String treeId;
+	Common::String realPath;
+	if (pos == Common::String::npos) {
+		treeId = path.substr(sizeof(SAF_MOUNT_POINT) - 1);
+	} else {
+		treeId = path.substr(sizeof(SAF_MOUNT_POINT) - 1, pos - sizeof(SAF_MOUNT_POINT) + 1);
+		realPath = path.substr(pos);
+	}
+
+	jobject safTree = JNI::findSAFTree(treeId);
+	if (!safTree) {
+		LOGW("AndroidSAFFilesystemNode::makeFromPath: tree id %s not found", treeId.c_str());
+		return nullptr;
+	}
+
+	JNIEnv *env = JNI::getEnv();
+
+	jstring pathObj = env->NewStringUTF(realPath.c_str());
+
+	jobject node = env->CallObjectMethod(safTree, _MID_pathToNode, pathObj);
+
+	env->DeleteLocalRef(pathObj);
+
+	if (env->ExceptionCheck()) {
+		LOGE("SAFFSTree::pathToNode failed");
+
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+
+		env->DeleteLocalRef(safTree);
+		return nullptr;
+	}
+
+	if (!node) {
+		env->DeleteLocalRef(safTree);
+		return nullptr;
+	}
+
+	AndroidSAFFilesystemNode *ret = new AndroidSAFFilesystemNode(safTree, node);
+
+	env->DeleteLocalRef(node);
+	env->DeleteLocalRef(safTree);
+
+	return ret;
+}
+
+AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromTree(jobject safTree) {
+	assert(safTree);
+
+	JNIEnv *env = JNI::getEnv();
+
+	jobject node = env->GetObjectField(safTree, _FID__root);
+	if (!node) {
+		env->DeleteLocalRef(safTree);
+		return nullptr;
+	}
+
+	AndroidSAFFilesystemNode *ret = new AndroidSAFFilesystemNode(safTree, node);
+
+	env->DeleteLocalRef(node);
+	env->DeleteLocalRef(safTree);
+
+	return ret;
+}
+
+AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(jobject safTree, jobject safNode) :
+	_cached(false), _flags(0), _safParent(nullptr) {
+
+	JNIEnv *env = JNI::getEnv();
+
+	_safTree = env->NewGlobalRef(safTree);
+	assert(_safTree);
+	_safNode = env->NewGlobalRef(safNode);
+	assert(_safNode);
+
+	cacheData();
+}
+
+AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(jobject safTree, jobject safParent,
+        const Common::String &path, const Common::String &name) :
+	_safNode(nullptr), _cached(false), _flags(0), _safParent(nullptr) {
+
+	JNIEnv *env = JNI::getEnv();
+
+	_safTree = env->NewGlobalRef(safTree);
+	assert(_safTree);
+	_safParent = env->NewGlobalRef(safParent);
+	assert(_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) {
+
+	JNIEnv *env = JNI::getEnv();
+
+	_safTree = env->NewGlobalRef(node._safTree);
+	assert(_safTree);
+
+	if (node._safNode) {
+		_safNode = env->NewGlobalRef(node._safNode);
+		assert(_safNode);
+	}
+
+	if (node._safParent) {
+		_safParent = env->NewGlobalRef(node._safParent);
+		assert(_safParent);
+	}
+
+	_cached = node._cached;
+	_path = node._path;
+	_flags = node._flags;
+	_newName = node._newName;
+}
+
+AndroidSAFFilesystemNode::~AndroidSAFFilesystemNode() {
+	JNIEnv *env = JNI::getEnv();
+
+	env->DeleteGlobalRef(_safTree);
+	env->DeleteGlobalRef(_safNode);
+	env->DeleteGlobalRef(_safParent);
+}
+
+Common::String AndroidSAFFilesystemNode::getName() const {
+	if (!_safNode || !_safParent) {
+		// _newName is for non-existent paths or root node pretty name
+		return _newName;
+	}
+
+	return lastPathComponent(_path, '/');
+}
+
+Common::String AndroidSAFFilesystemNode::getPath() const {
+	assert(_safTree != nullptr);
+
+	if (_safNode != nullptr) {
+		return _path;
+	}
+
+	// When no node, it means _path is the parent node
+	return _path + "/" + _newName;
+}
+
+AbstractFSNode *AndroidSAFFilesystemNode::getChild(const Common::String &n) const {
+	assert(_safTree != nullptr);
+	assert(_safNode != nullptr);
+
+	// Make sure the string contains no slashes
+	assert(!n.contains('/'));
+
+	JNIEnv *env = JNI::getEnv();
+
+	jstring name = env->NewStringUTF(n.c_str());
+
+	jobject child = env->CallObjectMethod(_safTree, _MID_getChild, _safNode, name);
+
+	env->DeleteLocalRef(name);
+
+	if (env->ExceptionCheck()) {
+		LOGE("SAFFSTree::getChild failed");
+
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+
+		return nullptr;
+	}
+
+	if (child) {
+		AndroidSAFFilesystemNode *ret = new AndroidSAFFilesystemNode(_safTree, child);
+		env->DeleteLocalRef(child);
+		return ret;
+	}
+
+	return new AndroidSAFFilesystemNode(_safTree, _safNode, _path, n);
+}
+
+bool AndroidSAFFilesystemNode::getChildren(AbstractFSList &myList, ListMode mode,
+        bool hidden) const {
+	assert(_flags & DIRECTORY);
+
+	assert(_safTree != nullptr);
+	if (!_safNode) {
+		return false;
+	}
+
+	JNIEnv *env = JNI::getEnv();
+
+	jobjectArray array =
+	    (jobjectArray)env->CallObjectMethod(_safTree, _MID_getChildren, _safNode);
+
+	if (env->ExceptionCheck()) {
+		LOGE("SAFFSTree::getChildren failed");
+
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+
+		return false;
+	}
+
+	myList.clear();
+
+	jsize size = env->GetArrayLength(array);
+	myList.reserve(size);
+
+	for (jsize i = 0; i < size; ++i) {
+		jobject node = env->GetObjectArrayElement(array, i);
+
+		myList.push_back(new AndroidSAFFilesystemNode(_safTree, node));
+
+		env->DeleteLocalRef(node);
+	}
+	env->DeleteLocalRef(array);
+
+	return true;
+}
+
+AbstractFSNode *AndroidSAFFilesystemNode::getParent() const {
+	assert(_safTree != nullptr);
+	assert(_safNode != nullptr);
+
+	if (_safParent) {
+		return new AndroidSAFFilesystemNode(_safTree, _safParent);
+	}
+
+	return AndroidFilesystemFactory::instance().makeRootFileNode();
+}
+
+Common::SeekableReadStream *AndroidSAFFilesystemNode::createReadStream() {
+	assert(_safTree != nullptr);
+
+	if (!_safNode) {
+		return nullptr;
+	}
+
+	JNIEnv *env = JNI::getEnv();
+
+	jint fd = env->CallIntMethod(_safTree, _MID_createReadStream, _safNode);
+
+	if (env->ExceptionCheck()) {
+		LOGE("SAFFSTree::createReadStream failed");
+
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+
+		return nullptr;
+	}
+
+	if (fd == -1) {
+		return nullptr;
+	}
+
+	FILE *f = fdopen(fd, "r");
+	if (!f) {
+		close(fd);
+		return nullptr;
+	}
+
+	return new PosixIoStream(f);
+}
+
+Common::SeekableWriteStream *AndroidSAFFilesystemNode::createWriteStream() {
+	assert(_safTree != nullptr);
+
+	JNIEnv *env = JNI::getEnv();
+
+	if (!_safNode) {
+		assert(_safParent);
+		jstring name = env->NewStringUTF(_newName.c_str());
+
+		jobject child = env->CallObjectMethod(_safTree, _MID_createFile, _safParent, name);
+
+		env->DeleteLocalRef(name);
+
+		if (env->ExceptionCheck()) {
+			LOGE("SAFFSTree::createFile failed");
+
+			env->ExceptionDescribe();
+			env->ExceptionClear();
+
+			return nullptr;
+		}
+
+		if (!child) {
+			return nullptr;
+		}
+
+		_safNode = env->NewGlobalRef(child);
+		assert(_safNode);
+
+		env->DeleteLocalRef(child);
+
+		cacheData(true);
+	}
+
+	jint fd = env->CallIntMethod(_safTree, _MID_createWriteStream, _safNode);
+	if (env->ExceptionCheck()) {
+		LOGE("SAFFSTree::createWriteStream failed");
+
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+
+		return nullptr;
+	}
+
+	if (fd == -1) {
+		return nullptr;
+	}
+
+	FILE *f = fdopen(fd, "w");
+	if (!f) {
+		close(fd);
+		return nullptr;
+	}
+
+	return new PosixIoStream(f);
+}
+
+bool AndroidSAFFilesystemNode::createDirectory() {
+	assert(_safTree != nullptr);
+
+	if (_safNode) {
+		return _flags & DIRECTORY;
+	}
+
+	assert(_safParent);
+
+	JNIEnv *env = JNI::getEnv();
+
+	jstring name = env->NewStringUTF(_newName.c_str());
+
+	jobject child = env->CallObjectMethod(_safTree, _MID_createDirectory, _safParent, name);
+
+	env->DeleteLocalRef(name);
+
+	if (env->ExceptionCheck()) {
+		LOGE("SAFFSTree::createDirectory failed");
+
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+
+		return false;
+	}
+
+	if (!child) {
+		return false;
+	}
+
+	_safNode = env->NewGlobalRef(child);
+	assert(_safNode);
+
+	env->DeleteLocalRef(child);
+
+	cacheData(true);
+
+	return true;
+}
+
+void AndroidSAFFilesystemNode::cacheData(bool force) {
+	if (_cached && !force) {
+		return;
+	}
+
+	JNIEnv *env = JNI::getEnv();
+
+	_flags = env->GetIntField(_safNode, _FID__flags);
+
+	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) {
+		jstring nameObj = (jstring)env->GetObjectField(_safTree, _FID__treeName);
+		const char *nameP = env->GetStringUTFChars(nameObj, 0);
+		if (nameP != 0) {
+			_newName = Common::String(nameP);
+			env->ReleaseStringUTFChars(nameObj, nameP);
+		}
+		env->DeleteLocalRef(nameObj);
+	}
+	_path.clear();
+
+	Common::String workingPath;
+
+	jstring pathObj = (jstring)env->GetObjectField(_safNode, _FID__path);
+	const char *path = env->GetStringUTFChars(pathObj, 0);
+	if (path != 0) {
+		workingPath = Common::String(path);
+		env->ReleaseStringUTFChars(pathObj, path);
+	} else {
+		env->DeleteLocalRef(pathObj);
+		return;
+	}
+	env->DeleteLocalRef(pathObj);
+
+	jstring idObj = (jstring)env->CallObjectMethod(_safTree, _MID_getTreeId);
+	if (env->ExceptionCheck()) {
+		LOGE("SAFFSTree::getTreeId failed");
+
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+
+		env->ReleaseStringUTFChars(pathObj, path);
+		env->DeleteLocalRef(pathObj);
+		return;
+	}
+
+	if (!idObj) {
+		return;
+	}
+
+	const char *id = env->GetStringUTFChars(idObj, 0);
+	if (id != 0) {
+		_path = Common::String::format("%s%s%s", SAF_MOUNT_POINT, id, workingPath.c_str());
+		env->ReleaseStringUTFChars(idObj, id);
+	}
+	env->DeleteLocalRef(idObj);
+
+	_cached = true;
+}
+
+const char AddSAFFakeNode::SAF_ADD_FAKE_PATH[] = "/saf";
+
+AddSAFFakeNode::~AddSAFFakeNode() {
+	delete _proxied;
+}
+
+AbstractFSNode *AddSAFFakeNode::getChild(const Common::String &name) const {
+	// We can't call getChild as it's protected
+	return nullptr;
+}
+
+AbstractFSNode *AddSAFFakeNode::getParent() const {
+	// We are always just below the root and getParent is protected
+	return AndroidFilesystemFactory::instance().makeRootFileNode();
+}
+
+bool AddSAFFakeNode::exists() const {
+	if (!_proxied) {
+		makeProxySAF();
+	}
+
+	if (!_proxied) {
+		return false;
+	}
+
+	return _proxied->exists();
+}
+
+bool AddSAFFakeNode::getChildren(AbstractFSList &list, ListMode mode, bool hidden) const {
+	if (!_proxied) {
+		makeProxySAF();
+	}
+
+	if (!_proxied) {
+		return false;
+	}
+
+	return _proxied->getChildren(list, mode, hidden);
+}
+
+Common::String AddSAFFakeNode::getPath() const {
+	if (!_proxied) {
+		makeProxySAF();
+	}
+
+	if (!_proxied) {
+		return "";
+	}
+
+	return _proxied->getPath();
+}
+
+bool AddSAFFakeNode::isReadable() const {
+	if (!_proxied) {
+		makeProxySAF();
+	}
+
+	if (!_proxied) {
+		return false;
+	}
+
+	return _proxied->isReadable();
+}
+
+bool AddSAFFakeNode::isWritable() const {
+	if (!_proxied) {
+		makeProxySAF();
+	}
+
+	if (!_proxied) {
+		return false;
+	}
+
+	return _proxied->isWritable();
+}
+
+void AddSAFFakeNode::makeProxySAF() const {
+	if (_proxied) {
+		return;
+	}
+
+	jobject saftree = JNI::getNewSAFTree(true, true, "", "Choose a new SAF tree");
+	if (!saftree) {
+		return;
+	}
+
+	_proxied = AndroidSAFFilesystemNode::makeFromTree(saftree);
+}
+
+#endif
diff --git a/backends/fs/android/android-saf-fs.h b/backends/fs/android/android-saf-fs.h
new file mode 100644
index 00000000000..2ec561e9c8f
--- /dev/null
+++ b/backends/fs/android/android-saf-fs.h
@@ -0,0 +1,183 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef ANDROID_SAF_FILESYSTEM_H
+#define ANDROID_SAF_FILESYSTEM_H
+
+#include <jni.h>
+#include "backends/fs/abstract-fs.h"
+
+/**
+ * Implementation of the ScummVM file system API.
+ *
+ * Parts of this class are documented in the base interface class, AbstractFSNode.
+ */
+class AndroidSAFFilesystemNode final : public AbstractFSNode {
+protected:
+	// SAFFSTree
+	static jmethodID _MID_getTreeId;
+	static jmethodID _MID_pathToNode;
+	static jmethodID _MID_getChildren;
+	static jmethodID _MID_getChild;
+	static jmethodID _MID_createDirectory;
+	static jmethodID _MID_createFile;
+	static jmethodID _MID_createReadStream;
+	static jmethodID _MID_createWriteStream;
+
+	static jfieldID _FID__treeName;
+	static jfieldID _FID__root;
+
+	// SAFFSNode
+	static jfieldID _FID__parent;
+	static jfieldID _FID__path;
+	static jfieldID _FID__documentId;
+	static jfieldID _FID__flags;
+
+	static bool _JNIinit;
+
+protected:
+	static const int DIRECTORY = 1;
+	static const int WRITABLE  = 2;
+	static const int READABLE  = 4;
+
+	jobject _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;
+
+	bool _cached;
+	Common::String _path;
+	int _flags;
+	jobject _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(jobject safTree, jobject safNode);
+
+public:
+	static const char SAF_MOUNT_POINT[];
+
+	/**
+	 * Init JNI parts related to SAF
+	 * Called by AndroidFilesystemFactory::AndroidFilesystemFactory()
+	 */
+	static void initJNI();
+
+	/**
+	 * Creates an AndroidSAFFilesystemNode given its absolute path
+	 *
+	 * @param path Path of the node
+	 */
+	static AndroidSAFFilesystemNode *makeFromPath(const Common::String &path);
+
+	/**
+	 * Creates an AndroidSAFFilesystemNode given its tree object
+	 * @param safTree SAF root in Java side. Must be a local reference and must not be used after this call.
+	 *
+	 */
+	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; }
+	Common::U32String getDisplayName() const override { return Common::U32String(getName()); }
+	Common::String getName() const override;
+	Common::String getPath() const override;
+	bool isDirectory() const override { return _flags & DIRECTORY; }
+	bool isReadable() const override { return _flags & READABLE; }
+	bool isWritable() const override { return _flags & WRITABLE; }
+
+	AbstractFSNode *getChild(const Common::String &n) const override;
+	bool getChildren(AbstractFSList &list, ListMode mode, bool hidden) const override;
+	AbstractFSNode *getParent() const override;
+
+	Common::SeekableReadStream *createReadStream() override;
+	Common::SeekableWriteStream *createWriteStream() override;
+	bool createDirectory() override;
+
+protected:
+	/**
+	 * Creates an non-existent AndroidSAFFilesystemNode given its tree, parent node and name
+	 *
+	 * @param safTree SAF root in Java side
+	 * @param safParent SAF parent node in Java side
+	 * @param path Parent path
+	 * @param name Item name
+	 */
+	AndroidSAFFilesystemNode(jobject safTree, jobject safParent,
+	                         const Common::String &path, const Common::String &name);
+
+	void cacheData(bool force = false);
+};
+
+class AddSAFFakeNode final : public AbstractFSNode {
+protected:
+	AbstractFSNode *getChild(const Common::String &name) const override;
+	AbstractFSNode *getParent() const override;
+
+public:
+	static const char SAF_ADD_FAKE_PATH[];
+
+	AddSAFFakeNode() : _proxied(nullptr) { }
+	~AddSAFFakeNode() override;
+
+	bool exists() const override;
+
+	bool getChildren(AbstractFSList &list, ListMode mode, bool hidden) const override;
+
+	Common::U32String getDisplayName() const override { return Common::U32String("\x01" "<Add SAF node>"); };
+	Common::String getName() const override { return "\x01" "<Add SAF node>"; };
+	Common::String getPath() const override;
+
+	bool isDirectory() const override { return true; }
+	bool isReadable() const override;
+	bool isWritable() const override;
+
+
+	Common::SeekableReadStream *createReadStream() override { return nullptr; }
+	virtual Common::SeekableWriteStream *createWriteStream() override { return nullptr; }
+
+	virtual bool createDirectory() { return false; }
+
+private:
+	void makeProxySAF() const;
+
+	mutable AbstractFSNode *_proxied;
+};
+#endif
diff --git a/backends/module.mk b/backends/module.mk
index 3b27cb913fa..2db8d9a113e 100644
--- a/backends/module.mk
+++ b/backends/module.mk
@@ -244,6 +244,9 @@ endif
 
 ifeq ($(BACKEND),android)
 MODULE_OBJS += \
+	fs/android/android-fs-factory.o \
+	fs/android/android-posix-fs.o \
+	fs/android/android-saf-fs.o \
 	graphics/android/android-graphics.o \
 	graphics3d/android/android-graphics3d.o \
 	graphics3d/android/texture.o \
diff --git a/backends/platform/android/android.cpp b/backends/platform/android/android.cpp
index ca7c3fce8e9..826c3364b9a 100644
--- a/backends/platform/android/android.cpp
+++ b/backends/platform/android/android.cpp
@@ -70,6 +70,7 @@
 #include "backends/graphics3d/android/android-graphics3d.h"
 #include "backends/platform/android/jni-android.h"
 #include "backends/platform/android/android.h"
+#include "backends/fs/android/android-fs-factory.h"
 
 const char *android_log_tag = "ScummVM";
 
@@ -144,8 +145,6 @@ OSystem_Android::OSystem_Android(int audio_sample_rate, int audio_buffer_size) :
 	_trackball_scale(2),
 	_joystick_scale(10) {
 
-	_fsFactory = new POSIXFilesystemFactory();
-
 	LOGI("Running on: [%s] [%s] [%s] [%s] [%s] SDK:%s ABI:%s",
 			getSystemProperty("ro.product.manufacturer").c_str(),
 			getSystemProperty("ro.product.model").c_str(),
@@ -160,6 +159,12 @@ OSystem_Android::OSystem_Android(int audio_sample_rate, int audio_buffer_size) :
 
 	int sdkVersion = JNI::getAndroidSDKVersionId();
 	LOGI("SDK Version: %d", sdkVersion);
+
+	AndroidFilesystemFactory &fsFactory = AndroidFilesystemFactory::instance();
+	if (sdkVersion >= 21) {
+		fsFactory.initSAF();
+	}
+	_fsFactory = &fsFactory;
 }
 
 OSystem_Android::~OSystem_Android() {
@@ -178,8 +183,8 @@ OSystem_Android::~OSystem_Android() {
 	_audiocdManager = 0;
 	delete _mixer;
 	_mixer = 0;
-	delete _fsFactory;
 	_fsFactory = 0;
+	AndroidFilesystemFactory::destroy();
 	delete _timerManager;
 	_timerManager = 0;
 
diff --git a/backends/platform/android/jni-android.cpp b/backends/platform/android/jni-android.cpp
index d04e6657348..ff023e018c2 100644
--- a/backends/platform/android/jni-android.cpp
+++ b/backends/platform/android/jni-android.cpp
@@ -97,10 +97,9 @@ jmethodID JNI::_MID_getSysArchives = 0;
 jmethodID JNI::_MID_getAllStorageLocations = 0;
 jmethodID JNI::_MID_initSurface = 0;
 jmethodID JNI::_MID_deinitSurface = 0;
-jmethodID JNI::_MID_createDirectoryWithSAF = 0;
-jmethodID JNI::_MID_createFileWithSAF = 0;
-jmethodID JNI::_MID_closeFileWithSAF = 0;
-jmethodID JNI::_MID_isDirectoryWritableWithSAF = 0;
+jmethodID JNI::_MID_getNewSAFTree = 0;
+jmethodID JNI::_MID_getSAFTrees = 0;
+jmethodID JNI::_MID_findSAFTree = 0;
 
 jmethodID JNI::_MID_EGL10_eglSwapBuffers = 0;
 
@@ -692,10 +691,10 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
 	FIND_METHOD(, initSurface, "()Ljavax/microedition/khronos/egl/EGLSurface;");
 	FIND_METHOD(, deinitSurface, "()V");
 	FIND_METHOD(, showSAFRevokePermsControl, "(Z)V");
-	FIND_METHOD(, createDirectoryWithSAF, "(Ljava/lang/String;)Z");
-	FIND_METHOD(, createFileWithSAF, "(Ljava/lang/String;)Ljava/lang/String;");
-	FIND_METHOD(, closeFileWithSAF, "(Ljava/lang/String;)V");
-	FIND_METHOD(, isDirectoryWritableWithSAF, "(Ljava/lang/String;)Z");
+	FIND_METHOD(, getNewSAFTree,
+	            "(ZZLjava/lang/String;Ljava/lang/String;)Lorg/scummvm/scummvm/SAFFSTree;");
+	FIND_METHOD(, getSAFTrees, "()[Lorg/scummvm/scummvm/SAFFSTree;");
+	FIND_METHOD(, findSAFTree, "(Ljava/lang/String;)Lorg/scummvm/scummvm/SAFFSTree;");
 
 	_jobj_egl = env->NewGlobalRef(egl);
 	_jobj_egl_display = env->NewGlobalRef(egl_display);
@@ -964,73 +963,75 @@ Common::Array<Common::String> JNI::getAllStorageLocations() {
 	return res;
 }
 
-bool JNI::createDirectoryWithSAF(const Common::String &dirPath) {
+jobject JNI::getNewSAFTree(bool folder, bool writable, const Common::String &initURI,
+                           const Common::String &prompt) {
 	JNIEnv *env = JNI::getEnv();
-	jstring javaDirPath = env->NewStringUTF(dirPath.c_str());
+	jstring javaInitURI = env->NewStringUTF(initURI.c_str());
+	jstring javaPrompt = env->NewStringUTF(prompt.c_str());
 
-	bool created = env->CallBooleanMethod(_jobj, _MID_createDirectoryWithSAF, javaDirPath);
+	jobject tree = env->CallObjectMethod(_jobj, _MID_getNewSAFTree,
+	                                     folder, writable, javaInitURI, javaPrompt);
 
 	if (env->ExceptionCheck()) {
-		LOGE("JNI - Failed to create directory with SAF enhanced method");
+		LOGE("getNewSAFTree: error");
 
 		env->ExceptionDescribe();
 		env->ExceptionClear();
-		created = false;
+
+		return nullptr;
 	}
 
-	return created;
+	env->DeleteLocalRef(javaInitURI);
+	env->DeleteLocalRef(javaPrompt);
+
+	return tree;
 }
 
-Common::U32String JNI::createFileWithSAF(const Common::String &filePath) {
-	JNIEnv *env = JNI::getEnv();
-	jstring javaFilePath = env->NewStringUTF(filePath.c_str());
+Common::Array<jobject> JNI::getSAFTrees() {
+	Common::Array<jobject> res;
 
-	jstring hackyFilenameJSTR = (jstring)env->CallObjectMethod(_jobj, _MID_createFileWithSAF, javaFilePath);
+	JNIEnv *env = JNI::getEnv();
 
+	jobjectArray array =
+	    (jobjectArray)env->CallObjectMethod(_jobj, _MID_getSAFTrees);
 
 	if (env->ExceptionCheck()) {
-		LOGE("JNI - Failed to create file with SAF enhanced method");
+		LOGE("getSAFTrees: error");
 
 		env->ExceptionDescribe();
 		env->ExceptionClear();
-		hackyFilenameJSTR = env->NewStringUTF("");
-	}
 
-	Common::U32String hackyFilenameStr = convertFromJString(env, hackyFilenameJSTR);
+		return res;
+	}
 
-	env->DeleteLocalRef(hackyFilenameJSTR);
+	jsize size = env->GetArrayLength(array);
+	for (jsize i = 0; i < size; ++i) {
+		jobject tree = env->GetObjectArrayElement(array, i);
+		res.push_back(tree);
+	}
+	env->DeleteLocalRef(array);
 
-	return hackyFilenameStr;
+	return res;
 }
 
-void JNI::closeFileWithSAF(const Common::String &hackyFilename) {
+jobject JNI::findSAFTree(const Common::String &name) {
 	JNIEnv *env = JNI::getEnv();
-	jstring javaHackyFilename = env->NewStringUTF(hackyFilename.c_str());
-
-	env->CallVoidMethod(_jobj, _MID_closeFileWithSAF, javaHackyFilename);
 
-	if (env->ExceptionCheck()) {
-		LOGE("JNI - Failed to close file with SAF enhanced method");
-
-		env->ExceptionDescribe();
-		env->ExceptionClear();
-	}
-}
+	jstring nameObj = env->NewStringUTF(name.c_str());
 
-bool JNI::isDirectoryWritableWithSAF(const Common::String &dirPath) {
-	JNIEnv *env = JNI::getEnv();
-	jstring javaDirPath = env->NewStringUTF(dirPath.c_str());
+	jobject tree = env->CallObjectMethod(_jobj, _MID_findSAFTree, nameObj);
 
-	bool isWritable = env->CallBooleanMethod(_jobj, _MID_isDirectoryWritableWithSAF, javaDirPath);
+	env->DeleteLocalRef(nameObj);
 
 	if (env->ExceptionCheck()) {
-		LOGE("JNI - Failed to check if directory is writable SAF enhanced method");
+		LOGE("findSAFTree: error");
 
 		env->ExceptionDescribe();
 		env->ExceptionClear();
-		isWritable = false;
+
+		return nullptr;
 	}
 
-	return isWritable;
+	return tree;
 }
 #endif
diff --git a/backends/platform/android/jni-android.h b/backends/platform/android/jni-android.h
index 2942e754fd4..e1daeceb91d 100644
--- a/backends/platform/android/jni-android.h
+++ b/backends/platform/android/jni-android.h
@@ -105,10 +105,9 @@ public:
 
 	static Common::Array<Common::String> getAllStorageLocations();
 
-	static bool createDirectoryWithSAF(const Common::String &dirPath);
-	static Common::U32String createFileWithSAF(const Common::String &filePath);
-	static void closeFileWithSAF(const Common::String &hackyFilename);
-	static bool isDirectoryWritableWithSAF(const Common::String &dirPath);
+	static jobject getNewSAFTree(bool folder, bool writable, const Common::String &initURI, const Common::String &prompt);
+	static Common::Array<jobject> getSAFTrees();
+	static jobject findSAFTree(const Common::String &name);
 
 private:
 	static pthread_key_t _env_tls;
@@ -144,10 +143,9 @@ private:
 	static jmethodID _MID_getAllStorageLocations;
 	static jmethodID _MID_initSurface;
 	static jmethodID _MID_deinitSurface;
-	static jmethodID _MID_createDirectoryWithSAF;
-	static jmethodID _MID_createFileWithSAF;
-	static jmethodID _MID_closeFileWithSAF;
-	static jmethodID _MID_isDirectoryWritableWithSAF;
+	static jmethodID _MID_getNewSAFTree;
+	static jmethodID _MID_getSAFTrees;
+	static jmethodID _MID_findSAFTree;
 
 	static jmethodID _MID_EGL10_eglSwapBuffers;
 
diff --git a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
new file mode 100644
index 00000000000..80cb8f173f4
--- /dev/null
+++ b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
@@ -0,0 +1,422 @@
+package org.scummvm.scummvm;
+
+import java.io.FileNotFoundException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.ListIterator;
+import java.util.Map;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.UriPermission;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * SAF primitives for C++ FSNode
+ */
+ at RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+public class SAFFSTree {
+	private static HashMap<String, SAFFSTree> _trees;
+
+	public static void loadSAFTrees(Context context) {
+		final ContentResolver resolver = context.getContentResolver();
+
+		_trees = new HashMap<String, SAFFSTree>();
+		for (UriPermission permission : resolver.getPersistedUriPermissions()) {
+			final Uri uri = permission.getUri();
+			if (!DocumentsContract.isTreeUri(uri)) {
+				continue;
+			}
+
+			SAFFSTree tree = new SAFFSTree(context, uri);
+			_trees.put(tree.getTreeId(), tree);
+		}
+	}
+
+	public static SAFFSTree newTree(Context context, Uri uri) {
+		if (_trees == null) {
+			loadSAFTrees(context);
+		}
+		SAFFSTree tree = new SAFFSTree(context, uri);
+		_trees.put(tree.getTreeId(), tree);
+		return tree;
+	}
+
+	public static SAFFSTree[] getTrees(Context context) {
+		if (_trees == null) {
+			loadSAFTrees(context);
+		}
+		return _trees.values().toArray(new SAFFSTree[0]);
+	}
+
+	public static SAFFSTree findTree(Context context, String name) {
+		if (_trees == null) {
+			loadSAFTrees(context);
+		}
+		return _trees.get(name);
+	}
+
+	public static class SAFFSNode {
+		public static final int DIRECTORY = 1;
+		public static final int WRITABLE  = 2;
+		public static final int READABLE  = 4;
+
+		public SAFFSNode _parent;
+		public String _path;
+		public String _documentId;
+		public int _flags;
+
+		private SAFFSNode() {
+		}
+
+		private SAFFSNode(SAFFSNode parent, String path, String documentId, int flags) {
+			_parent = parent;
+			_path = path;
+			_documentId = documentId;
+			_flags = flags;
+		}
+	}
+
+	// Sentinel object
+	private static final SAFFSNode NOT_FOUND_NODE = new SAFFSNode();
+
+	private static class SAFCache extends LinkedHashMap<String, SAFFSNode> {
+		private static final int MAX_ENTRIES = 1000;
+
+		public SAFCache() {
+			super(16, 0.75f, true);
+		}
+
+		@Override
+		protected boolean removeEldestEntry(Map.Entry<String, SAFFSNode> eldest) {
+			return size() > MAX_ENTRIES;
+		}
+	}
+
+	private Context _context;
+	private Uri _treeUri;
+
+	private SAFFSNode _root;
+	private String _treeName;
+
+	private SAFCache _cache;
+
+	public SAFFSTree(Context context, Uri treeUri) {
+		_context = context;
+		_treeUri = treeUri;
+
+		_cache = new SAFCache();
+
+		_root = new SAFFSNode(null, "", DocumentsContract.getTreeDocumentId(treeUri), 0);
+		// Update flags and get name
+		_treeName = stat(_root);
+		_cache.put("/", _root);
+		_cache.put("", _root);
+	}
+
+	public String getTreeId() {
+		return Uri.encode(DocumentsContract.getTreeDocumentId(_treeUri));
+	}
+
+	private static String[] normalizePath(String path) {
+		LinkedList<String> components = new LinkedList<String>(Arrays.asList(path.split("/")));
+		ListIterator<String> it = components.listIterator();
+		while(it.hasNext()) {
+			final String component = it.next();
+			if (component.isEmpty()) {
+				it.remove();
+				continue;
+			}
+			if (".".equals(component)) {
+				it.remove();
+				continue;
+			}
+			if ("..".equals(component)) {
+				it.remove();
+				if (it.hasPrevious()) {
+					it.previous();
+					it.remove();
+				}
+			}
+		}
+		return components.toArray(new String[0]);
+	}
+
+	public SAFFSNode pathToNode(String path) {
+		SAFFSNode node = null;
+
+		// Short-circuit
+		node = _cache.get(path);
+		if (node != null) {
+			if (node == NOT_FOUND_NODE) {
+				return null;
+			} else {
+				return node;
+			}
+		}
+
+		String[] components = normalizePath(path);
+
+		int pivot = components.length;
+		String wpath = "/" + String.join("/", components);
+
+		while(pivot > 0) {
+			node = _cache.get(wpath);
+			if (node != null) {
+				break;
+			}
+
+			// Try without last component
+			pivot--;
+			int newidx = wpath.length() - components[pivot].length() - 1;
+			wpath = wpath.substring(0, newidx);
+		}
+
+		// We found a negative result in cache for a point in the path
+		if (node == NOT_FOUND_NODE) {
+			wpath = "/" + String.join("/", components);
+			_cache.put(wpath, NOT_FOUND_NODE);
+			_cache.put(path, NOT_FOUND_NODE);
+			return null;
+		}
+
+		// Start from the last cached result (if any)
+		if (pivot == 0) {
+			node = _root;
+		}
+		while(pivot < components.length) {
+			node = getChild(node, components[pivot]);
+			if (node == null) {
+				// Cache as much as we can
+				wpath = "/" + String.join("/", components);
+				_cache.put(wpath, NOT_FOUND_NODE);
+				_cache.put(path, NOT_FOUND_NODE);
+				return null;
+			}
+
+			pivot++;
+		}
+
+		_cache.put(path, node);
+		return node;
+	}
+
+	public SAFFSNode[] getChildren(SAFFSNode node) {
+		final ContentResolver resolver = _context.getContentResolver();
+		final Uri searchUri = DocumentsContract.buildChildDocumentsUriUsingTree(_treeUri, node._documentId);
+		final LinkedList<SAFFSNode> results = new LinkedList<>();
+
+		Cursor c = null;
+		try {
+			c = resolver.query(searchUri, new String[] { DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+				DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE,
+				DocumentsContract.Document.COLUMN_FLAGS }, null, null, null);
+			while (c.moveToNext()) {
+				final String displayName = c.getString(0);
+				final String documentId = c.getString(1);
+				final String mimeType = c.getString(2);
+				final int flags = c.getInt(3);
+
+				int ourFlags = 0;
+				if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
+					ourFlags |= SAFFSNode.DIRECTORY;
+				}
+				if ((flags & (DocumentsContract.Document.FLAG_SUPPORTS_WRITE | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE)) != 0) {
+					ourFlags |= SAFFSNode.WRITABLE;
+				}
+				if ((flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) == 0) {
+					ourFlags |= SAFFSNode.READABLE;
+				}
+
+				SAFFSNode newnode = new SAFFSNode(node, node._path + "/" + displayName, documentId, ourFlags);
+				_cache.put(newnode._path, newnode);
+				results.add(newnode);
+			}
+		} catch (Exception e) {
+			Log.w(ScummVM.LOG_TAG, "Failed query: " + e);
+		} finally {
+			if (c != null) {
+				c.close();
+			}
+		}
+
+		return results.toArray(new SAFFSNode[0]);
+	}
+
+	public SAFFSNode getChild(SAFFSNode node, String name) {
+		final ContentResolver resolver = _context.getContentResolver();
+		final Uri searchUri = DocumentsContract.buildChildDocumentsUriUsingTree(_treeUri, node._documentId);
+
+		String childPath = node._path + "/" + name;
+		SAFFSNode newnode;
+
+		newnode = _cache.get(childPath);
+		if (newnode != null) {
+			if (newnode == NOT_FOUND_NODE) {
+				return null;
+			} else {
+				return newnode;
+			}
+		}
+
+		Cursor c = null;
+		try {
+			c = resolver.query(searchUri, new String[] { DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+				DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE,
+				DocumentsContract.Document.COLUMN_FLAGS }, null, null, null);
+			while (c.moveToNext()) {
+				final String displayName = c.getString(0);
+				if (!name.equals(displayName)) {
+					continue;
+				}
+
+				final String documentId = c.getString(1);
+				final String mimeType = c.getString(2);
+
+				final int flags = c.getInt(3);
+
+				int ourFlags = 0;
+				if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
+					ourFlags |= SAFFSNode.DIRECTORY;
+				}
+				if ((flags & (DocumentsContract.Document.FLAG_SUPPORTS_WRITE | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE)) != 0) {
+					ourFlags |= SAFFSNode.WRITABLE;
+				}
+				if ((flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) == 0) {
+					ourFlags |= SAFFSNode.READABLE;
+				}
+
+				newnode = new SAFFSNode(node, childPath, documentId, ourFlags);
+				_cache.put(newnode._path, newnode);
+				return newnode;
+			}
+		} catch (Exception e) {
+			Log.w(ScummVM.LOG_TAG, "Failed query: " + e);
+		} finally {
+			if (c != null) {
+				c.close();
+			}
+		}
+
+		_cache.put(childPath, NOT_FOUND_NODE);
+		return null;
+	}
+
+	public SAFFSNode createDirectory(SAFFSNode node, String name) {
+		return createDocument(node, name, DocumentsContract.Document.MIME_TYPE_DIR);
+	}
+
+	public SAFFSNode createFile(SAFFSNode node, String name) {
+		return createDocument(node, name, "application/octet-stream");
+	}
+
+	public int createReadStream(SAFFSNode node) {
+		return createStream(node, "r");
+	}
+
+	public int createWriteStream(SAFFSNode node) {
+		return createStream(node, "wt");
+	}
+
+	private SAFFSNode createDocument(SAFFSNode node, String name, String mimeType) {
+		final ContentResolver resolver = _context.getContentResolver();
+		final Uri parentUri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
+		Uri newDocUri;
+
+		try {
+			newDocUri = DocumentsContract.createDocument(resolver, parentUri, mimeType, name);
+		} catch(FileNotFoundException e) {
+			return null;
+		}
+		if (newDocUri == null) {
+			return null;
+		}
+
+		final String documentId = DocumentsContract.getDocumentId(newDocUri);
+
+		final SAFFSNode newnode = new SAFFSNode(node, node._path + "/" + name, documentId, 0);
+		// Update flags
+		final String realName = stat(_root);
+		if (realName == null) {
+			return null;
+		}
+		// Unlikely but...
+		if (!realName.equals(name)) {
+			newnode._path = node._path + "/" + realName;
+		}
+
+		_cache.put(newnode._path, newnode);
+
+		return newnode;
+	}
+
+	private int createStream(SAFFSNode node, String mode) {
+		final ContentResolver resolver = _context.getContentResolver();
+		final Uri uri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
+
+		ParcelFileDescriptor pfd;
+		try {
+			pfd = resolver.openFileDescriptor(uri, mode);
+		} catch(FileNotFoundException e) {
+			return -1;
+		}
+		if (pfd == null) {
+			return -1;
+		}
+
+		return pfd.detachFd();
+	}
+
+	private String stat(SAFFSNode node) {
+		final ContentResolver resolver = _context.getContentResolver();
+		final Uri uri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
+
+		Cursor c = null;
+		try {
+			c = resolver.query(uri, new String[] { DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+				DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_FLAGS }, null, null, null);
+			while (c.moveToNext()) {
+				final String displayName = c.getString(0);
+				final String mimeType = c.getString(1);
+				final int flags = c.getInt(2);
+
+				int ourFlags = 0;
+				if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
+					ourFlags |= SAFFSNode.DIRECTORY;
+				}
+				if ((flags & (DocumentsContract.Document.FLAG_SUPPORTS_WRITE | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE)) != 0) {
+					ourFlags |= SAFFSNode.WRITABLE;
+				}
+				if ((flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) == 0) {
+					ourFlags |= SAFFSNode.READABLE;
+				}
+
+				node._flags = ourFlags;
+				return displayName;
+			}
+		} catch (Exception e) {
+			Log.w(ScummVM.LOG_TAG, "Failed query: " + e);
+		} finally {
+			if (c != null) {
+				try {
+					c.close();
+				} catch (RuntimeException e) {
+					throw e;
+				} catch (Exception e) {
+				}
+			}
+		}
+		// We should never end up here...
+		return null;
+	}
+}
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVM.java b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
index 83b30c7f7cd..cb816d1791c 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVM.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
@@ -81,10 +81,9 @@ public abstract class ScummVM implements SurfaceHolder.Callback, Runnable {
 	abstract protected String[] getSysArchives();
 	abstract protected String[] getAllStorageLocations();
 	abstract protected String[] getAllStorageLocationsNoPermissionRequest();
-	abstract protected boolean createDirectoryWithSAF(String dirPath);
-	abstract protected String createFileWithSAF(String filePath);
-	abstract protected void closeFileWithSAF(String hackyFilename);
-	abstract protected boolean isDirectoryWritableWithSAF(String dirPath);
+	abstract protected SAFFSTree getNewSAFTree(boolean folder, boolean write, String initialURI, String prompt);
+	abstract protected SAFFSTree[] getSAFTrees();
+	abstract protected SAFFSTree findSAFTree(String name);
 
 	public ScummVM(AssetManager asset_manager, SurfaceHolder holder, final MyScummVMDestroyedCallback scummVMDestroyedCallback) {
 		_asset_manager = asset_manager;
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
index 1b35cfb92f1..9914b2bc278 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -9,6 +9,7 @@ import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.content.UriPermission;
 import android.content.pm.PackageManager;
 import android.content.res.AssetManager;
 import android.content.res.Configuration;
@@ -24,6 +25,7 @@ import android.os.Bundle;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.os.SystemClock;
+import android.provider.DocumentsContract;
 import android.text.TextUtils;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -45,7 +47,6 @@ import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
-import androidx.documentfile.provider.DocumentFile;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -83,9 +84,14 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 //	private File _usingLogFile;
 
 	// SAF related
-	private LinkedHashMap<String, ParcelFileDescriptor> hackyNameToOpenFileDescriptorList;
 	public final static int REQUEST_SAF = 50000;
 
+	// Use an Object to allow synchronization on it
+	private Object safSyncObject;
+	private int safRequestCode;
+	private int safResultCode;
+	private Uri safResultURI;
+
 	/**
 	 * Ids to identify an external storage read (and write) request.
 	 * They are app-defined int constants. The callback method gets the result of the request.
@@ -642,6 +648,7 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 			runOnUiThread(new Runnable() {
 				public void run() {
 					clearStorageAccessFrameworkTreeUri();
+					SAFFSTree.loadSAFTrees(ScummVMActivity.this);
 					_scummvm.displayMessageOnOSD(getString(R.string.saf_revoke_done));
 				}
 			});
@@ -830,178 +837,26 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 			return new String[0]; // an array of zero length
 		}
 
-		// In this method we first try the old method for creating directories (mkdirs())
-		// That should work with app spaces but will probably have issues with external physical "secondary" storage locations
-		// (eg user SD Card) on some devices, anyway.
 		@Override
-		protected boolean createDirectoryWithSAF(String dirPath) {
-			final boolean[] retRes = {false};
-
-			Log.d(ScummVM.LOG_TAG, "Attempt to create folder on path: " + dirPath);
-			File folderToCreate = new File (dirPath);
-//			if (folderToCreate.canWrite()) {
-//				Log.d(ScummVM.LOG_TAG, "This file node has write permission!" + dirPath);
-//			}
-//
-//			if (folderToCreate.canRead()) {
-//				Log.d(ScummVM.LOG_TAG, "This file node has read permission!" + dirPath);
-//
-//			}
-//
-//			if (folderToCreate.getParentFile() != null) {
-//				if( folderToCreate.getParentFile().canWrite()) {
-//					Log.d(ScummVM.LOG_TAG, "The parent of this node permits write operation!" + dirPath);
-//				}
-//
-//				if (folderToCreate.getParentFile().canRead()) {
-//					Log.d(ScummVM.LOG_TAG, "The parent of this node permits read operation!" + dirPath);
-//
-//				}
-//			}
-
-			if (folderToCreate.mkdirs()) {
-				Log.d(ScummVM.LOG_TAG, "Folder created with the simple mkdirs() command!");
-			} else {
-				Log.d(ScummVM.LOG_TAG, "Folder creation with mkdirs() failed!");
-				if (getStorageAccessFrameworkTreeUri() == null) {
-					requestStorageAccessFramework(dirPath);
-					Log.d(ScummVM.LOG_TAG, "Requested Storage Access via Storage Access Framework!");
-				} else {
-					Log.d(ScummVM.LOG_TAG, "Already requested Storage Access (Storage Access Framework) in the past (share prefs saved)!");
-				}
-
-				if (canWriteFile(folderToCreate, true)) {
-					// TODO we should only need the callback if we want to do something with the file descriptor
-					//  (the writeFile will close it afterwards if keepFileDescriptorOpen is false)
-					Log.d(ScummVM.LOG_TAG, "(post SAF request) Writing is possible for this directory node");
-					writeFile(folderToCreate, true, false, new MyWriteFileCallback() {
-						@Override
-						public void handle(Boolean created, String hackyFilename) {
-							//Log.d(ScummVM.LOG_TAG, "Via callback: file operation success: " + created);
-							retRes[0] = created;
-						}
-					});
-				} else {
-					Log.d(ScummVM.LOG_TAG, "(post SAF request) Error - writing is still not possible for this directory node");
-
-				}
+		protected SAFFSTree getNewSAFTree(boolean folder, boolean write, String initialURI, String prompt) {
+			Uri initialURI_ = Uri.parse(initialURI);
+			Uri uri = selectWithNativeUI(folder, write, initialURI_, prompt);
+			if (uri == null) {
+				return null;
 			}
 
-//			// debug purpose
-//			if (folderToCreate.canWrite()) {
-//				// This is expected to return false here (since we don't check via SAF here)
-//				Log.d(ScummVM.LOG_TAG, "(post SAF access) We can write in folder:" + dirPath);
-//			}
-//			if (folderToCreate.canRead()) {
-//				// This will probably return true (at least for Android 28 and below)
-//				Log.d(ScummVM.LOG_TAG, "(post SAF access) We can read from folder:" + dirPath);
-//
-//			}
-
-			return retRes[0];
+			return SAFFSTree.newTree(ScummVMActivity.this, uri);
 		}
 
-
-		// This is a simplified version of createDirectoryWithSAF
-		// TODO Maybe we could merge isDirectoryWritableWithSAF() with createDirectoryWithSAF() using an extra argument parameter
 		@Override
-		protected boolean isDirectoryWritableWithSAF(String dirPath) {
-			final boolean[] retRes = {false};
-
-			Log.d(ScummVM.LOG_TAG, "Check if folder writable: " + dirPath);
-			File folderToCheck = new File (dirPath);
-			if (folderToCheck.canWrite()) {
-				Log.d(ScummVM.LOG_TAG, "This path has write permission!" + dirPath);
-			} else {
-				Log.d(ScummVM.LOG_TAG, "Trying to get write access with SAF");
-				if (getStorageAccessFrameworkTreeUri() == null) {
-					requestStorageAccessFramework(dirPath);
-				} else {
-					Log.d(ScummVM.LOG_TAG, "Already requested Storage Access (Storage Access Framework) in the past (share prefs saved)!");
-				}
-			}
-
-			if (canWriteFile(folderToCheck, true)) {
-				Log.d(ScummVM.LOG_TAG, "(post SAF request) Writing is possible for this directory node");
-				retRes[0] = true;
-			} else {
-				Log.d(ScummVM.LOG_TAG, "(post SAF request) Error - writing is still not possible for this directory node");
-			}
-
-			return retRes[0];
+		protected SAFFSTree[] getSAFTrees() {
+			return SAFFSTree.getTrees(ScummVMActivity.this);
 		}
 
 		@Override
-		protected String createFileWithSAF(String filePath) {
-			final String[] retResStr = {""};
-			File fileToCreate = new File (filePath);
-
-			Log.d(ScummVM.LOG_TAG, "Attempting file creation for: " + filePath);
-
-			// normal (no SAF) file create attempt
-			boolean needToGoThroughSAF = false;
-			try {
-				if (fileToCreate.exists() || !fileToCreate.createNewFile()) {
-					Log.d(ScummVM.LOG_TAG, "The file already exists!");
-					// already existed
-				} else {
-					Log.d(ScummVM.LOG_TAG, "An empty file was created!");
-
-				}
-			} catch(Exception e) {
-				//e.printStackTrace();
-				needToGoThroughSAF = true;
-			}
-
-			if (needToGoThroughSAF) {
-				Log.d(ScummVM.LOG_TAG, "File creation with createNewFile() failed!");
-				if (getStorageAccessFrameworkTreeUri() == null) {
-					requestStorageAccessFramework(filePath);
-					Log.d(ScummVM.LOG_TAG, "Requested Storage Access via Storage Access Framework!");
-				}
-
-				if (canWriteFile(fileToCreate, false)) {
-					// TODO we should only need the callback if we want to do something with the file descriptor
-					//      (the writeFile will close it afterwards if keepFileDescriptorOpen is false)
-					//      we need the fileDescriptor open for the native to continue the write operation
-					Log.d(ScummVM.LOG_TAG, "(post SAF request check) File writing should be possible");
-					writeFile(fileToCreate, false, true, new MyWriteFileCallback() {
-						@Override
-						public void handle(Boolean created, String hackyFilename) {
-							//Log.d(ScummVM.LOG_TAG, "Via callback: file operation success: " + created + " :: " + hackyFilename);
-							if (created) {
-								retResStr[0] = hackyFilename;
-							} else {
-								retResStr[0] = "";
-							}
-						}
-					});
-				} else {
-					Log.e(ScummVM.LOG_TAG, "(post SAF request) Error - writing is still not possible for this directory node");
-				}
-			}
-			return retResStr[0];
-		}
-
-		@Override
-		protected void closeFileWithSAF(String hackyFileName) {
-			if (hackyNameToOpenFileDescriptorList.containsKey(hackyFileName)) {
-				ParcelFileDescriptor openFileDescriptor = hackyNameToOpenFileDescriptorList.get(hackyFileName);
-
-				Log.d(ScummVM.LOG_TAG, "Closing file descriptor for " + hackyFileName);
-				if (openFileDescriptor != null) {
-					try {
-						openFileDescriptor.close();
-					} catch (IOException e) {
-						Log.e(ScummVM.LOG_TAG, e.getMessage());
-						e.printStackTrace();
-					}
-				}
-				hackyNameToOpenFileDescriptorList.remove(hackyFileName);
-			}
+		protected SAFFSTree findSAFTree(String name) {
+			return SAFFSTree.findTree(ScummVMActivity.this, name);
 		}
-
-		// TODO do we also need SAF enabled methods for deletion (file/folder) and reading (for files), listing of files (for folders)?
 	}
 
 	private MyScummVM _scummvm;
@@ -1014,7 +869,7 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 
-		hackyNameToOpenFileDescriptorList = new LinkedHashMap<>();
+		safSyncObject = new Object();
 
 		hideSystemUI();
 
@@ -1220,23 +1075,6 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 
 		super.onDestroy();
 
-		// close any open file descriptors due to the SAF code
-		for (String hackyFileName : hackyNameToOpenFileDescriptorList.keySet()) {
-			Log.d(ScummVM.LOG_TAG, "Destroy: Closing file descriptor for " + hackyFileName);
-
-			ParcelFileDescriptor openFileDescriptor = hackyNameToOpenFileDescriptorList.get(hackyFileName);
-
-			if (openFileDescriptor != null) {
-				try {
-					openFileDescriptor.close();
-				} catch (IOException e) {
-					Log.e(ScummVM.LOG_TAG, e.getMessage());
-					e.printStackTrace();
-				}
-			}
-		}
-		hackyNameToOpenFileDescriptorList.clear();
-
 		if (_events != null) {
 			_events.clearEventHandler();
 			_events.sendQuitEvent();
@@ -2285,253 +2123,99 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 
 	// -------------------------------------------------------------------------------------------
 	// Start of SAF enabled code
-	// Code borrows parts from open source project: OpenLaucher's SharedUtil class
-	// https://github.com/OpenLauncherTeam/openlauncher
-	// https://github.com/OpenLauncherTeam/openlauncher/blob/master/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java
-	// as well as StackOverflow threads:
-	// https://stackoverflow.com/questions/43066117/android-m-write-to-sd-card-permission-denied
-	// https://stackoverflow.com/questions/59000390/android-accessing-files-in-native-c-c-code-with-google-scoped-storage-api
 	// -------------------------------------------------------------------------------------------
 	public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
-		if (resultCode != RESULT_OK)
-			return;
-		else {
-			if (requestCode == REQUEST_SAF) {
-				if (resultCode == RESULT_OK && resultData != null && resultData.getData() != null) {
-					Uri treeUri = resultData.getData();
-					//SharedPreferences sharedPref = getApplicationContext().getSharedPreferences(getApplicationContext().getPackageName() + "_preferences", Context.MODE_PRIVATE);
-					SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE);
-
-					SharedPreferences.Editor editor = sharedPref.edit();
-					editor.putString(getString(R.string.preference_saf_tree_key), treeUri.toString());
-					editor.apply();
-
-					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
-						getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
-					}
-					return;
-				}
-			}
-		}
-	}
-
-	/***
-	 * Request storage access. The user needs to press "Select storage" at the correct storage.
-	 */
-	public void requestStorageAccessFramework(String dirPathSample) {
-
-		_scummvm.displayMessageOnOSD(getString(R.string.saf_request_prompt) + dirPathSample);
-
-		if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
-			// Directory picker
-			Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
-			intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
-			                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-			                | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
-			                | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
-			);
-			startActivityForResult(intent, REQUEST_SAF);
-		}
-	}
-
-	/**
-	 * Get storage access framework tree uri. The user must have granted access via requestStorageAccessFramework
-	 *
-	 * @return Uri or null if not granted yet
-	 */
-	public Uri getStorageAccessFrameworkTreeUri() {
-		SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE);
-		String treeStr = sharedPref.getString(getString(R.string.preference_saf_tree_key), null);
-
-		if (!TextUtils.isEmpty(treeStr)) {
-			try {
-				Log.d(ScummVM.LOG_TAG, "getStorageAccessFrameworkTreeUri: " + treeStr);
-				return Uri.parse(treeStr);
-			} catch (Exception ignored) {
+		synchronized(safSyncObject) {
+			safRequestCode = requestCode;
+			safResultCode = resultCode;
+			safResultURI = null;
+			if (resultData != null) {
+				safResultURI = resultData.getData();
 			}
+			safSyncObject.notifyAll();
 		}
-		return null;
 	}
 
-	// A method to revoke SAF granted stored permissions
-	// TODO We need a button or setting to trigger this on user's demand
-	public void clearStorageAccessFrameworkTreeUri() {
-		if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
-			return;
+	// From: https://developer.android.com/training/data-storage/shared/documents-files
+	// Caution: If you iterate through a large number of files within the directory that's accessed using ACTION_OPEN_DOCUMENT_TREE, your app's performance might be reduced.
+	// Access restrictions
+	// On Android 11 (API level 30) and higher, you cannot use the ACTION_OPEN_DOCUMENT_TREE intent action to request access to the following directories:
+	// - The root directory of the internal storage volume.
+	// - The root directory of each SD card volume that the device manufacturer considers to be reliable, regardless of whether the card is emulated or removable. A reliable volume is one that an app can successfully access most of the time.
+	// - The Download directory.
+	// Furthermore, on Android 11 (API level 30) and higher, you cannot use the ACTION_OPEN_DOCUMENT_TREE intent action to request that the user select individual files from the following directories:
+	// - The Android/data/ directory and all subdirectories.
+	// - The Android/obb/ directory and all subdirectories.
+	public Uri selectWithNativeUI(boolean folder, boolean write, Uri initialURI, String prompt) {
+		// Choose a directory using the system's folder picker.
+		Intent intent;
+		if (folder) {
+			intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+		} else {
+			intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+			intent.addCategory(Intent.CATEGORY_OPENABLE);
+			intent.setType("*/*");
 		}
-
-		Uri treeUri;
-		if ((treeUri = getStorageAccessFrameworkTreeUri()) == null) {
-			return;
+		if (initialURI != null) {
+			intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialURI);
 		}
-
-		// revoke SAF permission AND clear the pertinent SharedPreferences key
-		getContentResolver().releasePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
-		SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE);
-		SharedPreferences.Editor editor = sharedPref.edit();
-		editor.remove(getString(R.string.preference_saf_tree_key));
-		editor.apply();
-	}
-
-	public File getStorageRootFolder(final File file) {
-		String filepath;
-		try {
-			filepath = file.getCanonicalPath();
-		} catch (Exception ignored) {
-			return null;
+		if (prompt != null) {
+			intent.putExtra(DocumentsContract.EXTRA_PROMPT, prompt);
 		}
 
-		for (String storagePath : _scummvm.getAllStorageLocationsNoPermissionRequest() ) {
-			if (filepath.startsWith(storagePath)) {
-				return new File(storagePath);
-			}
-		}
-		return null;
-	}
-
-	// TODO we need to implement support for reading access somewhere too
-	@SuppressWarnings({"ResultOfMethodCallIgnored", "StatementWithEmptyBody"})
-	public void writeFile(final File file, final boolean isDirectory, final boolean keepFileDescriptorOpen, final MyWriteFileCallback writeFileCallback ) {
-		try {
-			// TODO we need code for read access too (even though currently API28 reading works without SAF, just with the runtime permissions)
-			String hackyFilename = "";
-
-			ParcelFileDescriptor pfd = null;
-			if (file.canWrite() || (!file.exists() && file.getParentFile().canWrite())) {
-				if (isDirectory) {
-					file.mkdirs();
-				} else {
-					// If we are here this means creating a new file can be done with fopen from native
-					//fileOutputStream = new FileOutputStream(file);
-					Log.d(ScummVM.LOG_TAG, "writeFile() file can be created normally -- (not created here)" );
-					hackyFilename = "";
-				}
-			} else {
-				DocumentFile dof = getDocumentFile(file, isDirectory);
-				if (dof != null && dof.getUri() != null && dof.canWrite()) {
-					if (isDirectory) {
-						// Nothing more to do
-					} else {
-						pfd = getContentResolver().openFileDescriptor(dof.getUri(), "w");
-						if (pfd != null) {
-							// https://stackoverflow.com/questions/59000390/android-accessing-files-in-native-c-c-code-with-google-scoped-storage-api
-							int fd = pfd.getFd();
-							hackyFilename = "/proc/self/fd/" + fd;
-							hackyNameToOpenFileDescriptorList.put(hackyFilename, pfd);
-							Log.d(ScummVM.LOG_TAG, "writeFile() file created with SAF -- hacky name: " + hackyFilename );
-						}
-					}
-				}
-			}
-
-			// TODO the idea of a callback is to work with the output (or input) streams, then return here and close the streams and the descriptors properly
-			//      however since we are interacting with native this would not work for those cases
-
-			if (writeFileCallback != null) {
-				writeFileCallback.handle( (isDirectory && file.exists()) || (!isDirectory && file.exists() && file.isFile() ), hackyFilename);
-
-			}
-
-			// TODO We need to close the file descriptor when we are done with it from native
-			//		- what if the call is not from native but from the activity?
-			//      - directory operations don't create or need a file descriptor
-			if (!keepFileDescriptorOpen && pfd != null) {
-				if (hackyNameToOpenFileDescriptorList.containsKey(hackyFilename)) {
-					hackyNameToOpenFileDescriptorList.remove(hackyFilename);
+		int resultCode;
+		Uri resultURI;
+		synchronized(safSyncObject) {
+			safRequestCode = 0;
+			startActivityForResult(intent, REQUEST_SAF);
+			while(safRequestCode != REQUEST_SAF) {
+				try {
+					safSyncObject.wait();
+				} catch (InterruptedException e) {
+					Log.d(ScummVM.LOG_TAG, "Warning: interrupted while waiting for SAF");
+					return null;
 				}
-				pfd.close();
 			}
-		} catch (Exception e) {
-			e.printStackTrace();
-		}
-	}
+			resultCode = safResultCode;
+			resultURI = safResultURI;
 
-	/**
-	 * Get a DocumentFile object out of a normal java File object.
-	 * When used on a external storage (SD), use requestStorageAccessFramework()
-	 * first to get access. Otherwise this will fail.
-	 *
-	 * @param file  The file/folder to convert
-	 * @param isDir Whether or not file is a directory. For non-existing (to be created) files this info is not known hence required.
-	 * @return A DocumentFile object or null if file cannot be converted
-	 */
-	@SuppressWarnings("RegExpRedundantEscape")
-	public DocumentFile getDocumentFile(final File file, final boolean isDir) {
-		// On older versions use fromFile
-		if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
-			return DocumentFile.fromFile(file);
+			// Keep our URI safe from other calls
+			safResultURI = null;
 		}
 
-		// Get ContextUtils to find storageRootFolder
-		File baseFolderFile = getStorageRootFolder(file);
-
-		String baseFolder = baseFolderFile == null ? null : baseFolderFile.getAbsolutePath();
-		boolean originalDirectory = false;
-		if (baseFolder == null) {
+		if (resultCode != RESULT_OK) {
+			Log.d(ScummVM.LOG_TAG, "Warning: resultCode NOT OK for SAF selection!");
 			return null;
 		}
 
-		String relPath = null;
-		try {
-			String fullPath = file.getCanonicalPath();
-			if (!baseFolder.equals(fullPath)) {
-				relPath = fullPath.substring(baseFolder.length() + 1);
-			} else {
-				originalDirectory = true;
-			}
-		} catch (IOException e) {
+		if (resultURI == null) {
+			Log.d(ScummVM.LOG_TAG, "Warning: NO selected Folder URI!");
 			return null;
-		} catch (Exception ignored) {
-			originalDirectory = true;
 		}
 
-		Uri treeUri;
-		if ((treeUri = getStorageAccessFrameworkTreeUri()) == null) {
-			return null;
-		}
+		Log.d(ScummVM.LOG_TAG, "Selected SAF URI: " + resultURI.toString());
 
-		DocumentFile dof = DocumentFile.fromTreeUri(getApplicationContext(), treeUri);
-		if (originalDirectory) {
-			return dof;
+		int grant = Intent.FLAG_GRANT_READ_URI_PERMISSION;
+		if (write) {
+			grant |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
 		}
+		getContentResolver().takePersistableUriPermission(resultURI, grant);
 
-		// Important note: We cannot assume that anything sent here is a relative path on top of the *ONLY* SAF "root" path
-		//                 since the user could select another SD Card (from multiple inserted or replaces the current one and inserts another)
-		// TODO Can we translate our path string "/storage/XXXX-XXXXX/folder/doc.ext' a content URI? or a document URI?
-		String[] parts = relPath.split("\\/");
-		for (int i = 0; i < parts.length; i++) {
-			DocumentFile nextDof = dof.findFile(parts[i]);
-			if (nextDof == null) {
-				try {
-					nextDof = ((i < parts.length - 1) || isDir) ? dof.createDirectory(parts[i]) : dof.createFile("image", parts[i]);
-				} catch (Exception ignored) {
-					nextDof = null;
-				}
-			}
-			dof = nextDof;
-		}
-		return dof;
+		return resultURI;
 	}
 
-	/**
-	 * Check whether or not a file can be written.
-	 * Requires storage access framework permission for external storage (SD)
-	 *
-	 * @param file  The file object (file/folder)
-	 * @param isDirectory Whether or not the given file parameter is a directory
-	 * @return Whether or not the file can be written
-	 */
-	public boolean canWriteFile(final File file, final boolean isDirectory) {
-		if (file == null) {
-			return false;
-		} else if (file.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())
-		           || file.getAbsolutePath().startsWith(getFilesDir().getAbsolutePath())) {
-			return (!isDirectory && file.getParentFile() != null) ? file.getParentFile().canWrite() : file.canWrite();
-		} else {
-			DocumentFile dof = getDocumentFile(file, isDirectory);
-			return dof != null && dof.canWrite();
+	// A method to revoke SAF granted stored permissions
+	public void clearStorageAccessFrameworkTreeUri() {
+		if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
+			return;
+		}
+
+		for (UriPermission permission : getContentResolver().getPersistedUriPermissions()) {
+			getContentResolver().releasePersistableUriPermission(permission.getUri(),
+					Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
 		}
 	}
+
 	// -------------------------------------------------------------------------------------------
 	// End of SAF enabled code
 	// -------------------------------------------------------------------------------------------
@@ -2611,11 +2295,6 @@ abstract class SetLayerType {
 	}
 }
 
-// Used to define the interface for a callback after a write operation (via the method that is enhanced to use SAF if the normal way fails)
-interface MyWriteFileCallback {
-	public void handle(Boolean created, String hackyFilename);
-}
-
 // Used to define the interface for a callback after ScummVM thread has finished
 interface MyScummVMDestroyedCallback {
 	public void handle(int exitResult);
diff --git a/dists/android/build.gradle b/dists/android/build.gradle
index 4771c0ffab7..b64f1add5e7 100644
--- a/dists/android/build.gradle
+++ b/dists/android/build.gradle
@@ -100,6 +100,5 @@ android {
 
 dependencies {
     implementation "androidx.annotation:annotation:1.1.0"
-    implementation "androidx.documentfile:documentfile:1.0.1"
     implementation "androidx.appcompat:appcompat:1.2.0"
 }


Commit: f939e442bbedd3ebbd26c0afac1f79d9ad13f3f7
    https://github.com/scummvm/scummvm/commit/f939e442bbedd3ebbd26c0afac1f79d9ad13f3f7
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2023-01-20T14:14:03+01:00

Commit Message:
ANDROID: Increase SDK version

Changed paths:
    dists/android/build.gradle


diff --git a/dists/android/build.gradle b/dists/android/build.gradle
index b64f1add5e7..8164e75190c 100644
--- a/dists/android/build.gradle
+++ b/dists/android/build.gradle
@@ -24,7 +24,7 @@ tasks.withType(JavaCompile) {
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 29
+    compileSdkVersion 33
     buildToolsVersion "33.0.1"
     ndkVersion "21.3.6528147"
 
@@ -36,7 +36,7 @@ android {
         setProperty("archivesBaseName", "ScummVM")
 
         minSdkVersion 19
-        targetSdkVersion 29
+        targetSdkVersion 33
 
         versionName "2.7.0"
         versionCode 89


Commit: a465718c24d19721a438a54c1d38bd380818b9bf
    https://github.com/scummvm/scummvm/commit/a465718c24d19721a438a54c1d38bd380818b9bf
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2023-01-20T14:14:03+01:00

Commit Message:
ANDROID: Fix comment

For coherence sake

Changed paths:
    backends/fs/android/android-saf-fs.cpp
    backends/platform/android/android.cpp
    backends/platform/android/events.cpp
    backends/platform/android/jni-android.cpp
    backends/platform/android/options.cpp
    backends/platform/android/snprintf.cpp
    backends/platform/android/touchcontrols.cpp


diff --git a/backends/fs/android/android-saf-fs.cpp b/backends/fs/android/android-saf-fs.cpp
index 8d90cc9af3e..8c51f248569 100644
--- a/backends/fs/android/android-saf-fs.cpp
+++ b/backends/fs/android/android-saf-fs.cpp
@@ -32,7 +32,7 @@
 // which gets messed up by our override mechanism; this could
 // be avoided by either changing the Android SDK to use the equally
 // legal and valid
-//   __attribute__ ((format(printf, 3, 4)))
+//   __attribute__ ((format(__printf__, 3, 4)))
 // or by refining our printf override to use a varadic macro
 // (which then wouldn't be portable, though).
 // Anyway, for now we just disable the printf override globally
diff --git a/backends/platform/android/android.cpp b/backends/platform/android/android.cpp
index 826c3364b9a..d2d5e2b3bf8 100644
--- a/backends/platform/android/android.cpp
+++ b/backends/platform/android/android.cpp
@@ -33,7 +33,7 @@
 // which gets messed up by our override mechanism; this could
 // be avoided by either changing the Android SDK to use the equally
 // legal and valid
-//   __attribute__ ((format(printf, 3, 4)))
+//   __attribute__ ((format(__printf__, 3, 4)))
 // or by refining our printf override to use a varadic macro
 // (which then wouldn't be portable, though).
 // Anyway, for now we just disable the printf override globally
diff --git a/backends/platform/android/events.cpp b/backends/platform/android/events.cpp
index 31695253817..038c5e33507 100644
--- a/backends/platform/android/events.cpp
+++ b/backends/platform/android/events.cpp
@@ -31,7 +31,7 @@
 // which gets messed up by our override mechanism; this could
 // be avoided by either changing the Android SDK to use the equally
 // legal and valid
-//   __attribute__ ((format(printf, 3, 4)))
+//   __attribute__ ((format(__printf__, 3, 4)))
 // or by refining our printf override to use a varadic macro
 // (which then wouldn't be portable, though).
 // Anyway, for now we just disable the printf override globally
diff --git a/backends/platform/android/jni-android.cpp b/backends/platform/android/jni-android.cpp
index ff023e018c2..bd8bf14932d 100644
--- a/backends/platform/android/jni-android.cpp
+++ b/backends/platform/android/jni-android.cpp
@@ -32,7 +32,7 @@
 // which gets messed up by our override mechanism; this could
 // be avoided by either changing the Android SDK to use the equally
 // legal and valid
-//   __attribute__ ((format(printf, 3, 4)))
+//   __attribute__ ((format(__printf__, 3, 4)))
 // or by refining our printf override to use a varadic macro
 // (which then wouldn't be portable, though).
 // Anyway, for now we just disable the printf override globally
diff --git a/backends/platform/android/options.cpp b/backends/platform/android/options.cpp
index 8edb0e6e094..43c557d7068 100644
--- a/backends/platform/android/options.cpp
+++ b/backends/platform/android/options.cpp
@@ -29,7 +29,7 @@
 // which gets messed up by our override mechanism; this could
 // be avoided by either changing the Android SDK to use the equally
 // legal and valid
-//   __attribute__ ((format(printf, 3, 4)))
+//   __attribute__ ((format(__printf__, 3, 4)))
 // or by refining our printf override to use a varadic macro
 // (which then wouldn't be portable, though).
 // Anyway, for now we just disable the printf override globally
diff --git a/backends/platform/android/snprintf.cpp b/backends/platform/android/snprintf.cpp
index 2f2e2aa5caa..907f2b706a6 100644
--- a/backends/platform/android/snprintf.cpp
+++ b/backends/platform/android/snprintf.cpp
@@ -195,7 +195,7 @@
 // which gets messed up by our override mechanism; this could
 // be avoided by either changing the Android SDK to use the equally
 // legal and valid
-//   __attribute__ ((format(printf, 3, 4)))
+//   __attribute__ ((format(__printf__, 3, 4)))
 // or by refining our printf override to use a varadic macro
 // (which then wouldn't be portable, though).
 // Anyway, for now we just disable the printf override globally
diff --git a/backends/platform/android/touchcontrols.cpp b/backends/platform/android/touchcontrols.cpp
index 4712ac868cb..a06f7aab614 100644
--- a/backends/platform/android/touchcontrols.cpp
+++ b/backends/platform/android/touchcontrols.cpp
@@ -30,7 +30,7 @@
 // which gets messed up by our override mechanism; this could
 // be avoided by either changing the Android SDK to use the equally
 // legal and valid
-//   __attribute__ ((format(printf, 3, 4)))
+//   __attribute__ ((format(__printf__, 3, 4)))
 // or by refining our printf override to use a varadic macro
 // (which then wouldn't be portable, though).
 // Anyway, for now we just disable the printf override globally


Commit: f78e79fd4f9acb9c9a688b1d9cbbf8312bfccfae
    https://github.com/scummvm/scummvm/commit/f78e79fd4f9acb9c9a688b1d9cbbf8312bfccfae
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2023-01-20T14:14:03+01:00

Commit Message:
ANDROID: Clear SAF cache when activity is hidden

This allows to refresh when user changed the folders behind ScummVM back

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


diff --git a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
index 80cb8f173f4..b246cbaa788 100644
--- a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
+++ b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
@@ -66,6 +66,15 @@ public class SAFFSTree {
 		return _trees.get(name);
 	}
 
+	public static void clearCaches() {
+		if (_trees == null) {
+			return;
+		}
+		for (SAFFSTree tree : _trees.values()) {
+			tree.clearCache();
+		}
+	}
+
 	public static class SAFFSNode {
 		public static final int DIRECTORY = 1;
 		public static final int WRITABLE  = 2;
@@ -120,14 +129,19 @@ public class SAFFSTree {
 		_root = new SAFFSNode(null, "", DocumentsContract.getTreeDocumentId(treeUri), 0);
 		// Update flags and get name
 		_treeName = stat(_root);
-		_cache.put("/", _root);
-		_cache.put("", _root);
+		clearCache();
 	}
 
 	public String getTreeId() {
 		return Uri.encode(DocumentsContract.getTreeDocumentId(_treeUri));
 	}
 
+	private void clearCache() {
+		_cache.clear();
+		_cache.put("/", _root);
+		_cache.put("", _root);
+	}
+
 	private static String[] normalizePath(String path) {
 		LinkedList<String> components = new LinkedList<String>(Arrays.asList(path.split("/")));
 		ListIterator<String> it = components.listIterator();
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
index 9914b2bc278..46f361a705b 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -1066,6 +1066,7 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 	public void onStop() {
 //		Log.d(ScummVM.LOG_TAG, "onStop");
 
+		SAFFSTree.clearCaches();
 		super.onStop();
 	}
 


Commit: baf42ae7e6cc6f2e2591d60ca227c8f129f812ea
    https://github.com/scummvm/scummvm/commit/baf42ae7e6cc6f2e2591d60ca227c8f129f812ea
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2023-01-20T14:14:03+01:00

Commit Message:
ANDROID: Add a dialog to revoke SAF authorizations

Remove old all-in-one revoke authorizations process

Changed paths:
  R dists/android/res/drawable/ic_lock_icon.xml
    backends/fs/android/android-fs-factory.cpp
    backends/fs/android/android-fs-factory.h
    backends/fs/android/android-saf-fs.cpp
    backends/fs/android/android-saf-fs.h
    backends/platform/android/jni-android.cpp
    backends/platform/android/jni-android.h
    backends/platform/android/options.cpp
    backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
    backends/platform/android/org/scummvm/scummvm/ScummVM.java
    backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java


diff --git a/backends/fs/android/android-fs-factory.cpp b/backends/fs/android/android-fs-factory.cpp
index 538d415c501..e478b614548 100644
--- a/backends/fs/android/android-fs-factory.cpp
+++ b/backends/fs/android/android-fs-factory.cpp
@@ -70,20 +70,16 @@ AndroidFilesystemFactory::Config::Config(const AndroidFilesystemFactory *factory
 	_storages(JNI::getAllStorageLocations()) {
 }
 
-bool AndroidFilesystemFactory::Config::getDrives(AbstractFSList &list, bool hidden) const {
-	Common::Array<jobject> trees;
-	if (_factory->_withSAF) {
-		trees = JNI::getSAFTrees();
+void AndroidFilesystemFactory::getSAFTrees(AbstractFSList &list, bool allowSAFadd) const {
+	if (!_withSAF) {
+		// Nothing if no SAF
+		return;
 	}
 
-	list.reserve(trees.size() + _storages.size() / 2);
+	Common::Array<jobject> trees = JNI::getSAFTrees();
 
-	// For SAF
-	if (_factory->_withSAF) {
-		list.push_back(new AddSAFFakeNode());
-	}
+	list.reserve(trees.size() + (allowSAFadd ? 1 : 0));
 
-	// If _withSAF is false, trees will be empty
 	for (Common::Array<jobject>::iterator it = trees.begin(); it != trees.end(); it++) {
 		AbstractFSNode *node = AndroidSAFFilesystemNode::makeFromTree(*it);
 		if (!node) {
@@ -93,6 +89,18 @@ bool AndroidFilesystemFactory::Config::getDrives(AbstractFSList &list, bool hidd
 		list.push_back(node);
 	}
 
+	if (allowSAFadd) {
+		list.push_back(new AddSAFFakeNode());
+	}
+
+}
+
+bool AndroidFilesystemFactory::Config::getDrives(AbstractFSList &list, bool hidden) const {
+	// For SAF
+	_factory->getSAFTrees(list, true);
+
+	list.reserve(list.size() + _storages.size() / 2);
+
 	// For old POSIX way
 	for (Common::Array<Common::String>::const_iterator it = _storages.begin(); it != _storages.end(); ++it) {
 		const Common::String &driveName = *it;
diff --git a/backends/fs/android/android-fs-factory.h b/backends/fs/android/android-fs-factory.h
index 127f342163d..8b2b0d2641e 100644
--- a/backends/fs/android/android-fs-factory.h
+++ b/backends/fs/android/android-fs-factory.h
@@ -37,11 +37,13 @@ class AndroidFilesystemFactory final : public FilesystemFactory,
 protected:
 	AndroidFilesystemFactory();
 public:
-	void initSAF();
 	AbstractFSNode *makeRootFileNode() const override;
 	AbstractFSNode *makeCurrentDirectoryFileNode() const override;
 	AbstractFSNode *makeFileNodePath(const Common::String &path) const override;
 
+	void initSAF();
+	bool hasSAF() const { return _withSAF; }
+	void getSAFTrees(AbstractFSList &list, bool allowSAFadd) const;
 private:
 	struct Config : public DrivePOSIXFilesystemNode::Config {
 		Config(const AndroidFilesystemFactory *factory);
diff --git a/backends/fs/android/android-saf-fs.cpp b/backends/fs/android/android-saf-fs.cpp
index 8c51f248569..48b2f74e20e 100644
--- a/backends/fs/android/android-saf-fs.cpp
+++ b/backends/fs/android/android-saf-fs.cpp
@@ -64,6 +64,7 @@ jmethodID AndroidSAFFilesystemNode::_MID_createDirectory = 0;
 jmethodID AndroidSAFFilesystemNode::_MID_createFile = 0;
 jmethodID AndroidSAFFilesystemNode::_MID_createReadStream = 0;
 jmethodID AndroidSAFFilesystemNode::_MID_createWriteStream = 0;
+jmethodID AndroidSAFFilesystemNode::_MID_removeTree = 0;
 
 jfieldID AndroidSAFFilesystemNode::_FID__treeName = 0;
 jfieldID AndroidSAFFilesystemNode::_FID__root = 0;
@@ -106,6 +107,7 @@ void AndroidSAFFilesystemNode::initJNI() {
 	FIND_METHOD(, createFile, "(" SAFFSNodeSig "Ljava/lang/String;)" SAFFSNodeSig);
 	FIND_METHOD(, createReadStream, "(" SAFFSNodeSig ")I");
 	FIND_METHOD(, createWriteStream, "(" SAFFSNodeSig ")I");
+	FIND_METHOD(, removeTree, "()V");
 
 	FIND_FIELD(, _treeName, "Ljava/lang/String;");
 	FIND_FIELD(, _root, SAFFSNodeSig);
@@ -493,6 +495,21 @@ bool AndroidSAFFilesystemNode::createDirectory() {
 	return true;
 }
 
+void AndroidSAFFilesystemNode::removeTree() {
+	assert(_safParent == nullptr);
+
+	JNIEnv *env = JNI::getEnv();
+
+	env->CallVoidMethod(_safTree, _MID_removeTree);
+
+	if (env->ExceptionCheck()) {
+		LOGE("SAFFSTree::removeTree failed");
+
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+	}
+}
+
 void AndroidSAFFilesystemNode::cacheData(bool force) {
 	if (_cached && !force) {
 		return;
diff --git a/backends/fs/android/android-saf-fs.h b/backends/fs/android/android-saf-fs.h
index 2ec561e9c8f..bd82d6699b5 100644
--- a/backends/fs/android/android-saf-fs.h
+++ b/backends/fs/android/android-saf-fs.h
@@ -41,6 +41,7 @@ protected:
 	static jmethodID _MID_createFile;
 	static jmethodID _MID_createReadStream;
 	static jmethodID _MID_createWriteStream;
+	static jmethodID _MID_removeTree;
 
 	static jfieldID _FID__treeName;
 	static jfieldID _FID__root;
@@ -131,6 +132,11 @@ public:
 	Common::SeekableWriteStream *createWriteStream() override;
 	bool createDirectory() override;
 
+	/**
+	 * Removes the SAF tree.
+	 * Only works on the root node
+	 */
+	void removeTree();
 protected:
 	/**
 	 * Creates an non-existent AndroidSAFFilesystemNode given its tree, parent node and name
diff --git a/backends/platform/android/jni-android.cpp b/backends/platform/android/jni-android.cpp
index bd8bf14932d..8893e688633 100644
--- a/backends/platform/android/jni-android.cpp
+++ b/backends/platform/android/jni-android.cpp
@@ -92,7 +92,6 @@ jmethodID JNI::_MID_showKeyboardControl = 0;
 jmethodID JNI::_MID_getBitmapResource = 0;
 jmethodID JNI::_MID_setTouchMode = 0;
 jmethodID JNI::_MID_getTouchMode = 0;
-jmethodID JNI::_MID_showSAFRevokePermsControl = 0;
 jmethodID JNI::_MID_getSysArchives = 0;
 jmethodID JNI::_MID_getAllStorageLocations = 0;
 jmethodID JNI::_MID_initSurface = 0;
@@ -504,19 +503,6 @@ int JNI::getTouchMode() {
 	return mode;
 }
 
-void JNI::showSAFRevokePermsControl(bool enable) {
-	JNIEnv *env = JNI::getEnv();
-
-	env->CallVoidMethod(_jobj, _MID_showSAFRevokePermsControl, enable);
-
-	if (env->ExceptionCheck()) {
-		LOGE("Error trying to show the revoke SAF permissions button");
-
-		env->ExceptionDescribe();
-		env->ExceptionClear();
-	}
-}
-
 // The following adds assets folder to search set.
 // However searching and retrieving from "assets" on Android this is slow
 // so we also make sure to add the "path" directory, with a higher priority
@@ -690,7 +676,6 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
 	FIND_METHOD(, getAllStorageLocations, "()[Ljava/lang/String;");
 	FIND_METHOD(, initSurface, "()Ljavax/microedition/khronos/egl/EGLSurface;");
 	FIND_METHOD(, deinitSurface, "()V");
-	FIND_METHOD(, showSAFRevokePermsControl, "(Z)V");
 	FIND_METHOD(, getNewSAFTree,
 	            "(ZZLjava/lang/String;Ljava/lang/String;)Lorg/scummvm/scummvm/SAFFSTree;");
 	FIND_METHOD(, getSAFTrees, "()[Lorg/scummvm/scummvm/SAFFSTree;");
@@ -1034,4 +1019,5 @@ jobject JNI::findSAFTree(const Common::String &name) {
 
 	return tree;
 }
+
 #endif
diff --git a/backends/platform/android/jni-android.h b/backends/platform/android/jni-android.h
index e1daeceb91d..3f3d75d9a68 100644
--- a/backends/platform/android/jni-android.h
+++ b/backends/platform/android/jni-android.h
@@ -87,7 +87,6 @@ public:
 	static Graphics::Surface *getBitmapResource(BitmapResources resource);
 	static void setTouchMode(int touchMode);
 	static int getTouchMode();
-	static void showSAFRevokePermsControl(bool enable);
 	static void addSysArchivesToSearchSet(Common::SearchSet &s, int priority);
 	static jint getAndroidSDKVersionId();
 
@@ -138,7 +137,6 @@ private:
 	static jmethodID _MID_getBitmapResource;
 	static jmethodID _MID_setTouchMode;
 	static jmethodID _MID_getTouchMode;
-	static jmethodID _MID_showSAFRevokePermsControl;
 	static jmethodID _MID_getSysArchives;
 	static jmethodID _MID_getAllStorageLocations;
 	static jmethodID _MID_initSurface;
diff --git a/backends/platform/android/options.cpp b/backends/platform/android/options.cpp
index 43c557d7068..24439cda053 100644
--- a/backends/platform/android/options.cpp
+++ b/backends/platform/android/options.cpp
@@ -36,16 +36,23 @@
 // for the Android port
 #define FORBIDDEN_SYMBOL_EXCEPTION_printf
 
+#include "backends/fs/android/android-fs-factory.h"
+#include "backends/fs/android/android-saf-fs.h"
 #include "backends/platform/android/android.h"
 #include "backends/platform/android/jni-android.h"
 
 #include "gui/gui-manager.h"
 #include "gui/ThemeEval.h"
 #include "gui/widget.h"
+#include "gui/widgets/list.h"
 #include "gui/widgets/popup.h"
 
 #include "common/translation.h"
 
+enum {
+	kRemoveCmd = 'RemS',
+};
+
 class AndroidOptionsWidget final : public GUI::OptionsContainerWidget {
 public:
 	explicit AndroidOptionsWidget(GuiObject *boss, const Common::String &name, const Common::String &domain);
@@ -60,6 +67,7 @@ public:
 private:
 	// OptionsContainerWidget API
 	void defineLayout(GUI::ThemeEval &layouts, const Common::String &layoutName, const Common::String &overlayedLayout) const override;
+	void handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) override;
 
 	GUI::CheckboxWidget *_onscreenCheckbox;
 	GUI::StaticTextWidget *_preferredTouchModeDesc;
@@ -69,7 +77,6 @@ private:
 	GUI::PopUpWidget *_preferredTM2DGamesPopUp;
 	GUI::StaticTextWidget *_preferredTM3DGamesDesc;
 	GUI::PopUpWidget *_preferredTM3DGamesPopUp;
-	GUI::CheckboxWidget *_onscreenSAFRevokeCheckbox;
 
 	bool _enabled;
 
@@ -78,6 +85,24 @@ private:
 	void saveTouchMode(const Common::String &setting, uint32 touchMode);
 };
 
+class SAFRemoveDialog : public GUI::Dialog {
+public:
+	SAFRemoveDialog();
+	virtual ~SAFRemoveDialog();
+
+	void open() override;
+	void reflowLayout() override;
+
+	void handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) override;
+
+protected:
+	GUI::ListWidget   *_safList;
+	AbstractFSList    _safTrees;
+
+	void clearListing();
+	void updateListing();
+};
+
 enum {
 	kTouchModeDefault = -1,
 	kTouchModeTouchpad = 0,
@@ -122,10 +147,9 @@ AndroidOptionsWidget::AndroidOptionsWidget(GuiObject *boss, const Common::String
 	_preferredTM2DGamesPopUp->appendEntry(_("Gamepad emulation"), kTouchModeGamepad);
 	_preferredTM3DGamesPopUp->appendEntry(_("Gamepad emulation"), kTouchModeGamepad);
 
-	if (inAppDomain) {
+	if (inAppDomain && AndroidFilesystemFactory::instance().hasSAF()) {
 		// Only show this checkbox in Options (via Options... in the launcher), and not at game domain level (via Edit Game...)
-		// I18N: Show a button to revoke Storage Access Framework permissions for Android
-		_onscreenSAFRevokeCheckbox = new GUI::CheckboxWidget(widgetsBoss(), "AndroidOptionsDialog.SAFRevokePermsControl", _("Show SAF revoke permissions overlay button"));
+		(new GUI::ButtonWidget(widgetsBoss(), "AndroidOptionsDialog.ForgetSAFButton", _("Forget SAF authorization"), Common::U32String(), kRemoveCmd))->setTarget(this);
 	}
 }
 
@@ -158,13 +182,28 @@ void AndroidOptionsWidget::defineLayout(GUI::ThemeEval &layouts, const Common::S
 			.addWidget("TM3DGamesText", "OptionsLabel")
 			.addWidget("TM3DGames", "PopUp")
 		.closeLayout();
-	if (inAppDomain) {
-		layouts.addWidget("SAFRevokePermsControl", "Checkbox");
+	if (inAppDomain && AndroidFilesystemFactory::instance().hasSAF()) {
+		layouts.addWidget("ForgetSAFButton", "WideButton");
 	}
 	layouts.closeLayout()
 	    .closeDialog();
 }
 
+void AndroidOptionsWidget::handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) {
+	switch (cmd) {
+	case kRemoveCmd: {
+		if (!AndroidFilesystemFactory::instance().hasSAF()) {
+			break;
+		}
+		SAFRemoveDialog removeDlg;
+		removeDlg.runModal();
+		break;
+	}
+	default:
+		GUI::OptionsContainerWidget::handleCommand(sender, cmd, data);
+	}
+}
+
 uint32 AndroidOptionsWidget::loadTouchMode(const Common::String &setting, bool acceptDefault, uint32 defaultValue) {
 	if (!acceptDefault || ConfMan.hasKey(setting, _domain)) {
 		Common::String preferredTouchMode = ConfMan.get(setting, _domain);
@@ -193,10 +232,6 @@ void AndroidOptionsWidget::load() {
 	}
 	_preferredTM2DGamesPopUp->setSelectedTag(loadTouchMode("touch_mode_2d_games", !inAppDomain, kTouchModeTouchpad));
 	_preferredTM3DGamesPopUp->setSelectedTag(loadTouchMode("touch_mode_3d_games", !inAppDomain, kTouchModeGamepad));
-
-	if (inAppDomain) {
-		_onscreenSAFRevokeCheckbox->setState(ConfMan.getBool("onscreen_saf_revoke_btn", _domain));
-	}
 }
 
 void AndroidOptionsWidget::saveTouchMode(const Common::String &setting, uint32 touchMode) {
@@ -228,10 +263,6 @@ bool AndroidOptionsWidget::save() {
 		}
 		saveTouchMode("touch_mode_2d_games", _preferredTM2DGamesPopUp->getSelectedTag());
 		saveTouchMode("touch_mode_3d_games", _preferredTM3DGamesPopUp->getSelectedTag());
-
-		if (inAppDomain) {
-			ConfMan.setBool("onscreen_saf_revoke_btn", _onscreenSAFRevokeCheckbox->getState(), _domain);
-		}
 	} else {
 		ConfMan.removeKey("onscreen_control", _domain);
 
@@ -240,10 +271,6 @@ bool AndroidOptionsWidget::save() {
 		}
 		ConfMan.removeKey("touch_mode_2d_games", _domain);
 		ConfMan.removeKey("touch_mode_3d_games", _domain);
-
-		if (inAppDomain) {
-			ConfMan.removeKey("onscreen_saf_revoke_btn", _domain);
-		}
 	}
 
 	return true;
@@ -272,10 +299,6 @@ void AndroidOptionsWidget::setEnabled(bool e) {
 	_preferredTM2DGamesPopUp->setEnabled(e);
 	_preferredTM3DGamesDesc->setEnabled(e);
 	_preferredTM3DGamesPopUp->setEnabled(e);
-
-	if (inAppDomain) {
-		_onscreenSAFRevokeCheckbox->setEnabled(e);
-	}
 }
 
 
@@ -320,5 +343,103 @@ void OSystem_Android::applyTouchSettings(bool _3dMode, bool overlayShown) {
 
 void OSystem_Android::applyBackendSettings() {
 	JNI::showKeyboardControl(ConfMan.getBool("onscreen_control"));
-	JNI::showSAFRevokePermsControl(ConfMan.getBool("onscreen_saf_revoke_btn"));
+}
+
+SAFRemoveDialog::SAFRemoveDialog() : GUI::Dialog("SAFBrowser") {
+
+	// Add file list
+	_safList = new GUI::ListWidget(this, "SAFBrowser.List");
+	_safList->setNumberingMode(GUI::kListNumberingOff);
+	_safList->setEditable(false);
+
+	_backgroundType = GUI::ThemeEngine::kDialogBackgroundPlain;
+
+	// Buttons
+	new GUI::ButtonWidget(this, "SAFBrowser.Close", _("Close"), Common::U32String(), GUI::kCloseCmd);
+	new GUI::ButtonWidget(this, "SAFBrowser.Remove", _("Remove"), Common::U32String(), kRemoveCmd);
+}
+
+SAFRemoveDialog::~SAFRemoveDialog() {
+	clearListing();
+}
+
+void SAFRemoveDialog::open() {
+	// Call super implementation
+	Dialog::open();
+
+	updateListing();
+}
+
+void SAFRemoveDialog::reflowLayout() {
+	GUI::ThemeEval &layouts = *g_gui.xmlEval();
+	layouts.addDialog(_name, "GlobalOptions", -1, -1, 16)
+	        .addLayout(GUI::ThemeLayout::kLayoutVertical)
+	            .addPadding(16, 16, 16, 16)
+	            .addWidget("List", "")
+		    .addLayout(GUI::ThemeLayout::kLayoutVertical)
+			.addPadding(0, 0, 16, 0)
+			.addLayout(GUI::ThemeLayout::kLayoutHorizontal)
+				.addPadding(0, 0, 0, 0)
+				.addWidget("Remove", "Button")
+				.addSpace(-1)
+				.addWidget("Close", "Button")
+			.closeLayout()
+		    .closeLayout()
+		.closeLayout()
+	.closeDialog();
+
+	layouts.setVar("Dialog.SAFBrowser.Shading", 1);
+
+	Dialog::reflowLayout();
+}
+
+void SAFRemoveDialog::handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) {
+	switch (cmd) {
+	case kRemoveCmd:
+	{
+		int id = _safList->getSelected();
+		if (id == -1) {
+			break;
+		}
+
+		AndroidSAFFilesystemNode *node = reinterpret_cast<AndroidSAFFilesystemNode *>(_safTrees[id]);
+		node->removeTree();
+
+		updateListing();
+		break;
+	}
+	default:
+		GUI::Dialog::handleCommand(sender, cmd, data);
+	}
+}
+
+void SAFRemoveDialog::clearListing() {
+	for (AbstractFSList::iterator it = _safTrees.begin(); it != _safTrees.end(); it++) {
+		delete *it;
+	}
+	_safTrees.clear();
+}
+
+void SAFRemoveDialog::updateListing() {
+	int oldSel = _safList->getSelected();
+
+	clearListing();
+
+	AndroidFilesystemFactory::instance().getSAFTrees(_safTrees, false);
+
+	Common::U32StringArray list;
+	list.reserve(_safTrees.size());
+	for (AbstractFSList::iterator it = _safTrees.begin(); it != _safTrees.end(); it++) {
+		list.push_back((*it)->getDisplayName());
+	}
+
+	_safList->setList(list);
+	if (oldSel >= 0 && (size_t)oldSel < list.size()) {
+		_safList->setSelected(oldSel);
+	} else {
+		_safList->scrollTo(0);
+	}
+
+	// Finally, redraw
+	g_gui.scheduleTopDialogRedraw();
 }
diff --git a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
index b246cbaa788..8f22bfc2d01 100644
--- a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
+++ b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
@@ -11,6 +11,7 @@ import java.util.Map;
 import android.annotation.SuppressLint;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
 import android.content.UriPermission;
 import android.database.Cursor;
 import android.net.Uri;
@@ -342,6 +343,19 @@ public class SAFFSTree {
 		return createStream(node, "wt");
 	}
 
+	public void removeTree() {
+		final ContentResolver resolver = _context.getContentResolver();
+
+		String treeId = getTreeId();
+
+		resolver.releasePersistableUriPermission(_treeUri,
+			Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
+		if (_trees == null || _trees.remove(treeId) == null) {
+			loadSAFTrees(_context);
+		}
+	}
+
 	private SAFFSNode createDocument(SAFFSNode node, String name, String mimeType) {
 		final ContentResolver resolver = _context.getContentResolver();
 		final Uri parentUri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVM.java b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
index cb816d1791c..ba990998cff 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVM.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
@@ -77,7 +77,6 @@ public abstract class ScummVM implements SurfaceHolder.Callback, Runnable {
 	abstract protected Bitmap getBitmapResource(int resource);
 	abstract protected void setTouchMode(int touchMode);
 	abstract protected int getTouchMode();
-	abstract protected void showSAFRevokePermsControl(boolean enable);
 	abstract protected String[] getSysArchives();
 	abstract protected String[] getAllStorageLocations();
 	abstract protected String[] getAllStorageLocationsNoPermissionRequest();
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
index 46f361a705b..a8b99d95db5 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -126,7 +126,6 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 	private EditableSurfaceView _main_surface = null;
 	private ImageView _toggleTouchModeKeyboardBtnIcon = null;
 	private ImageView _openMenuBtnIcon = null;
-	private ImageView _revokeSafPermissionsBtnIcon = null;
 
 	public View _screenKeyboard = null;
 	static boolean keyboardWithoutTextInputShown = false;
@@ -642,19 +641,6 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 		}
 	};
 
-	public final View.OnClickListener revokeSafPermissionsBtnOnClickListener = new View.OnClickListener() {
-		@Override
-		public void onClick(View v) {
-			runOnUiThread(new Runnable() {
-				public void run() {
-					clearStorageAccessFrameworkTreeUri();
-					SAFFSTree.loadSAFTrees(ScummVMActivity.this);
-					_scummvm.displayMessageOnOSD(getString(R.string.saf_revoke_done));
-				}
-			});
-		}
-	};
-
 	private class MyScummVM extends ScummVM {
 
 		public MyScummVM(SurfaceHolder holder, final MyScummVMDestroyedCallback destroyedCallback) {
@@ -794,15 +780,6 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 			return _events.getTouchMode();
 		}
 
-		@Override
-		protected void showSAFRevokePermsControl(final boolean enable) {
-			runOnUiThread(new Runnable() {
-				public void run() {
-					showSAFRevokePermissionsBtnIcon(enable);
-				}
-			});
-		}
-
 		@Override
 		protected String[] getSysArchives() {
 			Log.d(ScummVM.LOG_TAG, "Adding to Search Archive: " + _actualScummVMDataDir.getPath());
@@ -904,11 +881,6 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 		buttonLayout.addView(_openMenuBtnIcon, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT));
 		buttonLayout.bringChildToFront(_openMenuBtnIcon);
 
-		_revokeSafPermissionsBtnIcon = new ImageView(this);
-		_revokeSafPermissionsBtnIcon.setImageResource(R.drawable.ic_lock_icon);
-		buttonLayout.addView(_revokeSafPermissionsBtnIcon, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT));
-		buttonLayout.bringChildToFront(_revokeSafPermissionsBtnIcon);
-
 		_main_surface.setFocusable(true);
 		_main_surface.setFocusableInTouchMode(true);
 		_main_surface.requestFocus();
@@ -1008,7 +980,6 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 			_toggleTouchModeKeyboardBtnIcon.setOnClickListener(touchModeKeyboardBtnOnClickListener);
 			_toggleTouchModeKeyboardBtnIcon.setOnLongClickListener(touchModeKeyboardBtnOnLongClickListener);
 			_openMenuBtnIcon.setOnClickListener(menuBtnOnClickListener);
-			_revokeSafPermissionsBtnIcon.setOnClickListener(revokeSafPermissionsBtnOnClickListener);
 
 			// Keyboard visibility listener - mainly to hide system UI if keyboard is shown and we return from Suspend to the Activity
 			setKeyboardVisibilityListener(this);
@@ -1094,7 +1065,6 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 			hideScreenKeyboard();
 		}
 		showToggleKeyboardBtnIcon(false);
-		showSAFRevokePermissionsBtnIcon(false);
 	}
 
 
@@ -1253,19 +1223,6 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 		}
 	}
 
-	// Show or hide the semi-transparent overlay button
-	// for revoking SAF permissions
-	// This is independent of the toggle keyboard icon and menu icon (which appear together currently in showToggleKeyboardBtnIcon())
-	private void showSAFRevokePermissionsBtnIcon(boolean show) {
-		if (_revokeSafPermissionsBtnIcon != null ) {
-			if (show) {
-				_revokeSafPermissionsBtnIcon.setVisibility(View.VISIBLE);
-			} else {
-				_revokeSafPermissionsBtnIcon.setVisibility(View.GONE);
-			}
-		}
-	}
-
 	// Listener to check for keyboard visibility changes
 	// https://stackoverflow.com/a/36259261
 	private void setKeyboardVisibilityListener(final OnKeyboardVisibilityListener onKeyboardVisibilityListener) {
@@ -2205,18 +2162,6 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 		return resultURI;
 	}
 
-	// A method to revoke SAF granted stored permissions
-	public void clearStorageAccessFrameworkTreeUri() {
-		if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
-			return;
-		}
-
-		for (UriPermission permission : getContentResolver().getPersistedUriPermissions()) {
-			getContentResolver().releasePersistableUriPermission(permission.getUri(),
-					Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
-		}
-	}
-
 	// -------------------------------------------------------------------------------------------
 	// End of SAF enabled code
 	// -------------------------------------------------------------------------------------------
diff --git a/dists/android/res/drawable/ic_lock_icon.xml b/dists/android/res/drawable/ic_lock_icon.xml
deleted file mode 100644
index 8ed59fcf735..00000000000
--- a/dists/android/res/drawable/ic_lock_icon.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<vector android:alpha="0.5" android:height="48dp"
-    android:viewportHeight="48" android:viewportWidth="48"
-    android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
-    <path android:fillColor="#ffffff" android:pathData="M10,20h30v25h-30z"/>
-    <path android:fillColor="#ffffff"
-        android:pathData="M24.1836,-0.1387C22.4171,-0.1698 20.6862,0.2104 19.0703,1.0039 16.2909,2.3688 14.0478,4.9097 12.5781,8.3594c-0.2786,0.654 -0.7844,2.1021 -0.9727,2.7871l-0.0918,0.3379 2.2363,-0.0117 2.2383,-0.0117 0.2461,-0.5371c1.048,-2.2753 2.5856,-4.0879 4.4082,-5.1992 0.8166,-0.4979 1.7899,-0.8821 2.7891,-1.0977 0.4245,-0.0916 0.6295,-0.1039 1.5391,-0.1055 0.9158,-0.0016 1.115,0.0113 1.5801,0.1055 1.4601,0.2957 2.8223,0.9677 4.0605,2.0039 1.635,1.3683 2.9994,3.5111 3.832,6.0176 0.5823,1.7528 0.9676,3.981 1.0391,5.998l0.0313,0.8496h1.9707,1.9727v-0.4082c-0.0006,-1.2624 -0.2448,-3.3084 -0.584,-4.8867C37.5886,8.2233 34.1417,3.4317 29.5117,1.1875 27.7518,0.3344 25.9501,-0.1075 24.1836,-0.1387Z" android:strokeWidth="0.0465853"/>
-    <path android:fillColor="#ffffff"
-        android:pathData="M10.0624,32.5165V20.0317h14.9073,14.9073v12.4849,12.4849h-14.9073,-14.9073z" android:strokeWidth="0.186341"/>
-    <path android:fillColor="#ffffff"
-        android:pathData="M35.5103,17.8442C35.1329,13.1932 33.6772,9.5353 31.2341,7.0994 27.5554,3.4314 22.1709,3.54 18.5722,7.3548 17.8965,8.0711 16.9975,9.2878 16.5745,10.0586l-0.7691,1.4014H13.7724c-1.705,0 -2.0329,-0.0462 -2.0329,-0.2865 0,-0.5967 1.3591,-3.7746 2.1735,-5.082 1.7542,-2.8164 3.8975,-4.5919 6.719,-5.5661 2.6996,-0.9321 6.0179,-0.6265 8.8984,0.8194 3.4307,1.722 6.4099,5.0833 8.0319,9.0617 1.0465,2.5667 1.7557,5.8582 1.7557,8.1479v0.9182H37.4802,35.6424Z" android:strokeWidth="0.186341"/>
-</vector>




More information about the Scummvm-git-logs mailing list