[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