[Scummvm-git-logs] scummvm master -> 038a13b0f9a5a8b9aa6f4faa196170e03ad02aa0
lephilousophe
noreply at scummvm.org
Thu Apr 17 15:33:37 UTC 2025
This automated email contains information about 9 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
f68d539176 ANDROID: Upgrade Gadle and Android Gradle Plugin
5356618bac ANDROID: Remove superfluous argument
ddfe04e035 ANDROID: Check API version before using EXTRA_INITIAL_URI
e1391b03d2 ANDROID: Don't close the inner inflater stream
bbfb504b69 ANDROID: Refactor virtual path resolution
37104bb601 ANDROID: Various SAFFSTree improvements
b3531434d7 ANDROID: Improve selectWithNativeUI
97cf96532f ANDROID: Preserve entries order in Java INI parser
038a13b0f9 ANDROID: Allow users to backup and restore their configuration and saves
Commit: f68d539176a33ed2e1294b8d12d913dd7a17aa2f
https://github.com/scummvm/scummvm/commit/f68d539176a33ed2e1294b8d12d913dd7a17aa2f
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-04-17T17:33:31+02:00
Commit Message:
ANDROID: Upgrade Gadle and Android Gradle Plugin
Also fix deprecation warnings.
Changed paths:
dists/android/build.gradle
dists/android/gradle/wrapper/gradle-wrapper.jar
dists/android/gradle/wrapper/gradle-wrapper.properties
dists/android/gradlew
diff --git a/dists/android/build.gradle b/dists/android/build.gradle
index dcf561ba1c9..4bcf42f39df 100644
--- a/dists/android/build.gradle
+++ b/dists/android/build.gradle
@@ -5,7 +5,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.7.3'
+ classpath 'com.android.tools.build:gradle:8.9.1'
}
}
@@ -30,10 +30,10 @@ tasks.withType(JavaCompile).configureEach {
apply plugin: 'com.android.application'
android {
- compileSdk 35
- ndkVersion "23.2.8568313"
+ compileSdk = 35
+ ndkVersion = "23.2.8568313"
- namespace "org.scummvm.scummvm"
+ namespace = "org.scummvm.scummvm"
defaultConfig {
applicationId "org.scummvm.scummvm"
@@ -123,7 +123,7 @@ android {
}
}
lint {
- abortOnError false
+ abortOnError = false
}
if (project.hasProperty('splitAssets')) {
diff --git a/dists/android/gradle/wrapper/gradle-wrapper.jar b/dists/android/gradle/wrapper/gradle-wrapper.jar
index d64cd491770..a4b76b9530d 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 df97d72b8b9..37f853b1c84 100644
--- a/dists/android/gradle/wrapper/gradle-wrapper.properties
+++ b/dists/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/dists/android/gradlew b/dists/android/gradlew
index 1aa94a42690..f5feea6d6b1 100755
--- a/dists/android/gradlew
+++ b/dists/android/gradlew
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
Commit: 5356618bac3b52919c48c7a7959a0225eff0bbcc
https://github.com/scummvm/scummvm/commit/5356618bac3b52919c48c7a7959a0225eff0bbcc
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-04-17T17:33:31+02:00
Commit Message:
ANDROID: Remove superfluous argument
As getNewSAFTree always returns a tree, asking for a file makes no
sense.
Changed paths:
backends/fs/android/android-saf-fs.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
diff --git a/backends/fs/android/android-saf-fs.cpp b/backends/fs/android/android-saf-fs.cpp
index 28715d1c90f..fafd4e5b346 100644
--- a/backends/fs/android/android-saf-fs.cpp
+++ b/backends/fs/android/android-saf-fs.cpp
@@ -915,7 +915,7 @@ void AddSAFFakeNode::makeProxySAF() const {
}
// I18N: This may be displayed in the Android UI used to add a Storage Attach Framework authorization
- jobject saftree = JNI::getNewSAFTree(true, true, "", _("Choose a new folder"));
+ jobject saftree = JNI::getNewSAFTree(true, "", _("Choose a new folder"));
if (!saftree) {
return;
}
diff --git a/backends/platform/android/jni-android.cpp b/backends/platform/android/jni-android.cpp
index 156e0cb290e..1092dc8162e 100644
--- a/backends/platform/android/jni-android.cpp
+++ b/backends/platform/android/jni-android.cpp
@@ -779,7 +779,7 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
FIND_METHOD(, deinitSurface, "()V");
FIND_METHOD(, eglVersion, "()I");
FIND_METHOD(, getNewSAFTree,
- "(ZZLjava/lang/String;Ljava/lang/String;)Lorg/scummvm/scummvm/SAFFSTree;");
+ "(ZLjava/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;");
@@ -1069,14 +1069,14 @@ Common::Array<Common::String> JNI::getAllStorageLocations() {
return res;
}
-jobject JNI::getNewSAFTree(bool folder, bool writable, const Common::String &initURI,
+jobject JNI::getNewSAFTree(bool writable, const Common::String &initURI,
const Common::String &prompt) {
JNIEnv *env = JNI::getEnv();
jstring javaInitURI = env->NewStringUTF(initURI.c_str());
jstring javaPrompt = env->NewStringUTF(prompt.c_str());
jobject tree = env->CallObjectMethod(_jobj, _MID_getNewSAFTree,
- folder, writable, javaInitURI, javaPrompt);
+ writable, javaInitURI, javaPrompt);
if (env->ExceptionCheck()) {
LOGE("getNewSAFTree: error");
diff --git a/backends/platform/android/jni-android.h b/backends/platform/android/jni-android.h
index e2697b53788..90fea250c98 100644
--- a/backends/platform/android/jni-android.h
+++ b/backends/platform/android/jni-android.h
@@ -121,7 +121,7 @@ public:
static Common::Array<Common::String> getAllStorageLocations();
- static jobject getNewSAFTree(bool folder, bool writable, const Common::String &initURI, const Common::String &prompt);
+ static jobject getNewSAFTree(bool writable, const Common::String &initURI, const Common::String &prompt);
static Common::Array<jobject> getSAFTrees();
static jobject findSAFTree(const Common::String &name);
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVM.java b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
index a21af60e1d6..e32087afbef 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVM.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
@@ -96,7 +96,7 @@ public abstract class ScummVM implements SurfaceHolder.Callback,
abstract protected String[] getSysArchives();
abstract protected String[] getAllStorageLocations();
abstract protected String[] getAllStorageLocationsNoPermissionRequest();
- abstract protected SAFFSTree getNewSAFTree(boolean folder, boolean write, String initialURI, String prompt);
+ abstract protected SAFFSTree getNewSAFTree(boolean write, String initialURI, String prompt);
abstract protected SAFFSTree[] getSAFTrees();
abstract protected SAFFSTree findSAFTree(String name);
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
index 32f44cb8b3d..5e3699e0378 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -893,9 +893,9 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
@Override
@RequiresApi(api = Build.VERSION_CODES.N)
- protected SAFFSTree getNewSAFTree(boolean folder, boolean write, String initialURI, String prompt) {
+ protected SAFFSTree getNewSAFTree(boolean write, String initialURI, String prompt) {
Uri initialURI_ = Uri.parse(initialURI);
- Uri uri = selectWithNativeUI(folder, write, initialURI_, prompt);
+ Uri uri = selectWithNativeUI(true, write, initialURI_, prompt);
if (uri == null) {
return null;
}
Commit: ddfe04e035bc2a2021959c82ad22511f88f4a9ff
https://github.com/scummvm/scummvm/commit/ddfe04e035bc2a2021959c82ad22511f88f4a9ff
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-04-17T17:33:31+02:00
Commit Message:
ANDROID: Check API version before using EXTRA_INITIAL_URI
Changed paths:
backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
index 5e3699e0378..4c529ff3799 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -2281,7 +2281,7 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
}
- if (initialURI != null) {
+ if (initialURI != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialURI);
}
if (prompt != null) {
Commit: e1391b03d23fa8144c85d44ea9c401dc2ca4848e
https://github.com/scummvm/scummvm/commit/e1391b03d23fa8144c85d44ea9c401dc2ca4848e
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-04-17T17:33:31+02:00
Commit Message:
ANDROID: Don't close the inner inflater stream
Closing it makes an error when trying to recycle the Inflater.
Changed paths:
backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java
diff --git a/backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java b/backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java
index fb4c9a219ad..e0aef375c95 100644
--- a/backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java
+++ b/backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java
@@ -530,7 +530,14 @@ public class ZipFile implements ZipConstants, Closeable {
if (closeRequested)
return;
closeRequested = true;
- super.close();
+ /* ScummVM-changed:
+ don't call InflaterInputStream.close as it closes the Inflater.
+ This doesn't happen in Android because they pass ownsInflater to false but this
+ function is hidden to us.
+ Directly call the underlying input stream close function instead.
+ */
+ //super.close();
+ in.close();
synchronized (res.istreams) {
res.istreams.remove(this);
}
Commit: bbfb504b696f28c177f740b4826ae054425b9230
https://github.com/scummvm/scummvm/commit/bbfb504b696f28c177f740b4826ae054425b9230
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-04-17T17:33:31+02:00
Commit Message:
ANDROID: Refactor virtual path resolution
Makes it available to other uses as this will avoid code duplication.
Changed paths:
backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
diff --git a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
index 5c700a18d16..4392ea39108 100644
--- a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
+++ b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
@@ -144,6 +144,47 @@ public class SAFFSTree {
return _trees.get(name);
}
+ public static class PathResult {
+ public final SAFFSTree tree;
+ public final SAFFSNode node;
+
+ PathResult(SAFFSTree tree, SAFFSNode node) {
+ this.tree = tree;
+ this.node = node;
+ }
+ }
+
+ /**
+ * Resolves a ScummVM virtual path to SAF objects if it's in the SAF domain.
+ * Returns null otherwise and throws a FileNotFoundException if the SAF path doesn't exist.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.BASE)
+ public static PathResult fullPathToNode(Context context, String path) throws FileNotFoundException {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
+ !path.startsWith("/saf/")) {
+ return null;
+ }
+
+ // This is a SAF fake mount point
+ int slash = path.indexOf('/', 5);
+ if (slash == -1) {
+ slash = path.length();
+ }
+ String treeName = path.substring(5, slash);
+ String innerPath = path.substring(slash);
+
+ SAFFSTree tree = SAFFSTree.findTree(context, treeName);
+ if (tree == null) {
+ throw new FileNotFoundException();
+ }
+ SAFFSNode node = tree.pathToNode(innerPath);
+ if (node == null) {
+ throw new FileNotFoundException();
+ }
+
+ return new PathResult(tree, node);
+ }
+
@RequiresApi(api = Build.VERSION_CODES.BASE)
public static void clearCaches() {
if (_trees == null) {
diff --git a/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java b/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
index b0931031024..4bc878eebca 100644
--- a/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
@@ -176,8 +176,15 @@ public class ShortcutCreatorActivity extends Activity implements CompatHelpers.S
}
static private FileInputStream[] openFiles(Context context, File basePath, String regex) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
- !basePath.getPath().startsWith("/saf/")) {
+ SAFFSTree.PathResult pr;
+ try {
+ pr = SAFFSTree.fullPathToNode(context, basePath.getPath());
+ } catch (FileNotFoundException e) {
+ return new FileInputStream[0];
+ }
+
+ // This version check is only to make Android Studio linter happy
+ if (pr == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
// This is a standard filesystem path
File[] children = basePath.listFiles((dir, name) -> name.matches(regex));
if (children == null) {
@@ -192,24 +199,9 @@ public class ShortcutCreatorActivity extends Activity implements CompatHelpers.S
}
return ret;
}
- // This is a SAF fake mount point
- String baseName = basePath.getPath();
- int slash = baseName.indexOf('/', 5);
- if (slash == -1) {
- slash = baseName.length();
- }
- String treeName = baseName.substring(5, slash);
- String path = baseName.substring(slash);
- SAFFSTree tree = SAFFSTree.findTree(context, treeName);
- if (tree == null) {
- return new FileInputStream[0];
- }
- SAFFSTree.SAFFSNode node = tree.pathToNode(path);
- if (node == null) {
- return new FileInputStream[0];
- }
- SAFFSTree.SAFFSNode[] children = tree.getChildren(node);
+ // This is a SAF fake mount point
+ SAFFSTree.SAFFSNode[] children = pr.tree.getChildren(pr.node);
if (children == null) {
return new FileInputStream[0];
}
@@ -224,7 +216,7 @@ public class ShortcutCreatorActivity extends Activity implements CompatHelpers.S
if (!component.matches(regex)) {
continue;
}
- ParcelFileDescriptor pfd = tree.createFileDescriptor(child, "r");
+ ParcelFileDescriptor pfd = pr.tree.createFileDescriptor(child, "r");
if (pfd == null) {
continue;
}
Commit: 37104bb60106a183a3c45a37eb319a377f0e37fc
https://github.com/scummvm/scummvm/commit/37104bb60106a183a3c45a37eb319a377f0e37fc
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-04-17T17:33:31+02:00
Commit Message:
ANDROID: Various SAFFSTree improvements
These new functions will be needed soon.
The loadSAFTrees function could be dangerous and should not be used from
outside SAFFSTree.
Changed paths:
backends/fs/android/android-saf-fs.cpp
backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
diff --git a/backends/fs/android/android-saf-fs.cpp b/backends/fs/android/android-saf-fs.cpp
index fafd4e5b346..527fd98247f 100644
--- a/backends/fs/android/android-saf-fs.cpp
+++ b/backends/fs/android/android-saf-fs.cpp
@@ -125,7 +125,7 @@ void AndroidSAFFilesystemNode::initJNI() {
FIND_STATIC_METHOD(, refToNode, "(J)" SAFFSNodeSig);
FIND_METHOD(, getTreeId, "()Ljava/lang/String;");
- FIND_METHOD(, pathToNode, "(Ljava/lang/String;)" SAFFSNodeSig);
+ FIND_METHOD(, pathToNode, "(Ljava/lang/String;Z)" SAFFSNodeSig);
FIND_METHOD(, getChildren, "(J)[" SAFFSNodeSig);
FIND_METHOD(, getChild, "(JLjava/lang/String;)" SAFFSNodeSig);
FIND_METHOD(, createDirectory, "(JLjava/lang/String;)" SAFFSNodeSig);
@@ -287,7 +287,7 @@ AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromPath(const Common::S
jstring pathObj = env->NewStringUTF(realPath.c_str());
- jobject node = env->CallObjectMethod(safTree, _MID_pathToNode, pathObj);
+ jobject node = env->CallObjectMethod(safTree, _MID_pathToNode, pathObj, false);
env->DeleteLocalRef(pathObj);
@@ -332,7 +332,7 @@ AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromPath(const Common::S
pathObj = env->NewStringUTF(realPath.c_str());
- node = env->CallObjectMethod(safTree, _MID_pathToNode, pathObj);
+ node = env->CallObjectMethod(safTree, _MID_pathToNode, pathObj, false);
env->DeleteLocalRef(pathObj);
diff --git a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
index 4392ea39108..f27f05a467c 100644
--- a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
+++ b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
@@ -99,7 +99,7 @@ public class SAFFSTree {
}
}
- public static void loadSAFTrees(Context context) {
+ private static void loadSAFTrees(Context context) {
final ContentResolver resolver = context.getContentResolver();
// As this function is called before starting to emit nodes,
@@ -159,7 +159,7 @@ public class SAFFSTree {
* Returns null otherwise and throws a FileNotFoundException if the SAF path doesn't exist.
*/
@RequiresApi(api = Build.VERSION_CODES.BASE)
- public static PathResult fullPathToNode(Context context, String path) throws FileNotFoundException {
+ public static PathResult fullPathToNode(Context context, String path, boolean createDirIfNotExists) throws FileNotFoundException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
!path.startsWith("/saf/")) {
return null;
@@ -177,7 +177,7 @@ public class SAFFSTree {
if (tree == null) {
throw new FileNotFoundException();
}
- SAFFSNode node = tree.pathToNode(innerPath);
+ SAFFSNode node = tree.pathToNode(innerPath, createDirIfNotExists);
if (node == null) {
throw new FileNotFoundException();
}
@@ -315,6 +315,12 @@ public class SAFFSTree {
public String getTreeId() {
return Uri.encode(DocumentsContract.getTreeDocumentId(_treeUri));
}
+ public String getTreeName() {
+ return _treeName;
+ }
+ public Uri getTreeDocumentUri() {
+ return DocumentsContract.buildDocumentUriUsingTree(_treeUri, _root._documentId);
+ }
private void clearCache() {
ArrayDeque<SAFFSNode> stack = new ArrayDeque<>();
@@ -334,7 +340,7 @@ public class SAFFSTree {
}
}
- public SAFFSNode pathToNode(String path) {
+ public SAFFSNode pathToNode(String path, boolean createDirIfNotExists) {
String[] components = path.split("/");
SAFFSNode node = _root;
@@ -349,10 +355,14 @@ public class SAFFSTree {
continue;
}
- node = getChild(node, component);
- if (node == null) {
+ SAFFSNode newNode = getChild(node, component);
+ if (newNode == null && createDirIfNotExists) {
+ newNode = createDirectory(node, component);
+ }
+ if (newNode == null) {
return null;
}
+ node = newNode;
}
return node;
}
diff --git a/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java b/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
index 4bc878eebca..02f0cd4ad7c 100644
--- a/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
@@ -178,7 +178,7 @@ public class ShortcutCreatorActivity extends Activity implements CompatHelpers.S
static private FileInputStream[] openFiles(Context context, File basePath, String regex) {
SAFFSTree.PathResult pr;
try {
- pr = SAFFSTree.fullPathToNode(context, basePath.getPath());
+ pr = SAFFSTree.fullPathToNode(context, basePath.getPath(), false);
} catch (FileNotFoundException e) {
return new FileInputStream[0];
}
Commit: b3531434d708c00b50ebdcd9d1023f46c979f25e
https://github.com/scummvm/scummvm/commit/b3531434d708c00b50ebdcd9d1023f46c979f25e
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-04-17T17:33:31+02:00
Commit Message:
ANDROID: Improve selectWithNativeUI
Allow to specify a MIME type and a default file name.
Also use the proper Inten action to save a file.
Changed paths:
backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
index 4c529ff3799..b0a4d11d163 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -895,7 +895,7 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
@RequiresApi(api = Build.VERSION_CODES.N)
protected SAFFSTree getNewSAFTree(boolean write, String initialURI, String prompt) {
Uri initialURI_ = Uri.parse(initialURI);
- Uri uri = selectWithNativeUI(true, write, initialURI_, prompt);
+ Uri uri = selectWithNativeUI(true, write, initialURI_, prompt, null, null);
if (uri == null) {
return null;
}
@@ -2271,15 +2271,22 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
// - The Android/data/ directory and all subdirectories.
// - The Android/obb/ directory and all subdirectories.
@RequiresApi(api = Build.VERSION_CODES.N)
- public Uri selectWithNativeUI(boolean folder, boolean write, Uri initialURI, String prompt) {
+ public Uri selectWithNativeUI(boolean folder, boolean write, Uri initialURI, String prompt, String mimeType, String fileName) {
// 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);
+ if (write) {
+ intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ if (fileName != null) {
+ intent.putExtra(Intent.EXTRA_TITLE, fileName);
+ }
+ } else {
+ intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ }
intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("*/*");
+ intent.setType(mimeType);
}
if (initialURI != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialURI);
Commit: 97cf96532fe83c2c24e935b3c07857bd2a564e9f
https://github.com/scummvm/scummvm/commit/97cf96532fe83c2c24e935b3c07857bd2a564e9f
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-04-17T17:33:31+02:00
Commit Message:
ANDROID: Preserve entries order in Java INI parser
This will make sure every domain is always enumerated in the same order.
Changed paths:
backends/platform/android/org/scummvm/scummvm/INIParser.java
diff --git a/backends/platform/android/org/scummvm/scummvm/INIParser.java b/backends/platform/android/org/scummvm/scummvm/INIParser.java
index 3aed5e06660..ede8745f268 100644
--- a/backends/platform/android/org/scummvm/scummvm/INIParser.java
+++ b/backends/platform/android/org/scummvm/scummvm/INIParser.java
@@ -6,7 +6,7 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.Map;
/**
@@ -20,7 +20,7 @@ public class INIParser {
}
public static Map<String, Map<String, String>> parse(Reader reader) throws IOException {
- Map<String, Map<String, String>> ret = new HashMap<>();
+ Map<String, Map<String, String>> ret = new LinkedHashMap<>();
BufferedReader lineReader = new BufferedReader(reader);
Map<String, String> domain = null;
int lineno = 0;
@@ -64,7 +64,7 @@ public class INIParser {
}
String domainName = line.substring(1, i);
- domain = new HashMap<>();
+ domain = new LinkedHashMap<>();
ret.put(domainName, domain);
continue;
Commit: 038a13b0f9a5a8b9aa6f4faa196170e03ad02aa0
https://github.com/scummvm/scummvm/commit/038a13b0f9a5a8b9aa6f4faa196170e03ad02aa0
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2025-04-17T17:33:31+02:00
Commit Message:
ANDROID: Allow users to backup and restore their configuration and saves
This also try to restore the SAF authorizations registered in the
backup.
With this, a user can wipe the application and reinstall it without too
much hassle.
Changed paths:
A backends/platform/android/org/scummvm/scummvm/BackupManager.java
backends/platform/android/jni-android.cpp
backends/platform/android/jni-android.h
backends/platform/android/options.cpp
backends/platform/android/org/scummvm/scummvm/ScummVM.java
backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
diff --git a/backends/platform/android/jni-android.cpp b/backends/platform/android/jni-android.cpp
index 1092dc8162e..ccfd57d0a05 100644
--- a/backends/platform/android/jni-android.cpp
+++ b/backends/platform/android/jni-android.cpp
@@ -107,6 +107,8 @@ jmethodID JNI::_MID_eglVersion = 0;
jmethodID JNI::_MID_getNewSAFTree = 0;
jmethodID JNI::_MID_getSAFTrees = 0;
jmethodID JNI::_MID_findSAFTree = 0;
+jmethodID JNI::_MID_exportBackup = 0;
+jmethodID JNI::_MID_importBackup = 0;
jmethodID JNI::_MID_EGL10_eglSwapBuffers = 0;
@@ -782,6 +784,8 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
"(ZLjava/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;");
+ FIND_METHOD(, exportBackup, "(Ljava/lang/String;)I");
+ FIND_METHOD(, importBackup, "(Ljava/lang/String;Ljava/lang/String;)I");
_jobj_egl = env->NewGlobalRef(egl);
_jobj_egl_display = env->NewGlobalRef(egl_display);
@@ -1140,3 +1144,53 @@ jobject JNI::findSAFTree(const Common::String &name) {
return tree;
}
+
+int JNI::exportBackup(const Common::U32String &prompt) {
+ JNIEnv *env = JNI::getEnv();
+
+ jstring promptObj = convertToJString(env, prompt);
+
+ jint result = env->CallIntMethod(_jobj, _MID_exportBackup, promptObj);
+
+ env->DeleteLocalRef(promptObj);
+
+ if (env->ExceptionCheck()) {
+ LOGE("exportBackup: error");
+
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+
+ // BackupManager.ERROR_INVALID_BACKUP
+ return -1;
+ }
+
+ return result;
+}
+
+int JNI::importBackup(const Common::U32String &prompt, const Common::String &path) {
+ JNIEnv *env = JNI::getEnv();
+
+ jstring promptObj = convertToJString(env, prompt);
+
+ jstring pathObj = nullptr;
+ if (!path.empty()) {
+ pathObj = env->NewStringUTF(path.c_str());
+ }
+
+ jint result = env->CallIntMethod(_jobj, _MID_importBackup, promptObj, pathObj);
+
+ env->DeleteLocalRef(pathObj);
+ env->DeleteLocalRef(promptObj);
+
+ if (env->ExceptionCheck()) {
+ LOGE("importBackup: error");
+
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+
+ // BackupManager.ERROR_INVALID_BACKUP
+ return -1;
+ }
+
+ return result;
+}
diff --git a/backends/platform/android/jni-android.h b/backends/platform/android/jni-android.h
index 90fea250c98..bf486a927e4 100644
--- a/backends/platform/android/jni-android.h
+++ b/backends/platform/android/jni-android.h
@@ -125,6 +125,9 @@ public:
static Common::Array<jobject> getSAFTrees();
static jobject findSAFTree(const Common::String &name);
+ static int exportBackup(const Common::U32String &prompt);
+ static int importBackup(const Common::U32String &prompt, const Common::String &path);
+
private:
static pthread_key_t _env_tls;
@@ -168,6 +171,8 @@ private:
static jmethodID _MID_getNewSAFTree;
static jmethodID _MID_getSAFTrees;
static jmethodID _MID_findSAFTree;
+ static jmethodID _MID_exportBackup;
+ static jmethodID _MID_importBackup;
static jmethodID _MID_EGL10_eglSwapBuffers;
diff --git a/backends/platform/android/options.cpp b/backends/platform/android/options.cpp
index f1bd868c864..7bdcc945049 100644
--- a/backends/platform/android/options.cpp
+++ b/backends/platform/android/options.cpp
@@ -41,7 +41,9 @@
#include "backends/fs/android/android-fs-factory.h"
#include "backends/fs/android/android-saf-fs.h"
+#include "gui/browser.h"
#include "gui/gui-manager.h"
+#include "gui/message.h"
#include "gui/ThemeEval.h"
#include "gui/widget.h"
#include "gui/widgets/list.h"
@@ -51,6 +53,8 @@
enum {
kRemoveCmd = 'RemS',
+ kExportBackupCmd = 'ExpD',
+ kImportBackupCmd = 'ImpD',
};
class AndroidOptionsWidget final : public GUI::OptionsContainerWidget {
@@ -183,10 +187,14 @@ AndroidOptionsWidget::AndroidOptionsWidget(GuiObject *boss, const Common::String
_orientationGamesPopUp->appendEntry(_("Portrait"), kOrientationPortrait);
_orientationGamesPopUp->appendEntry(_("Landscape"), kOrientationLandscape);
- 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: This button opens a list of all folders added for Android Storage Attached Framework
- (new GUI::ButtonWidget(widgetsBoss(), "AndroidOptionsDialog.ForgetSAFButton", _("Remove folder authorizations..."), Common::U32String(), kRemoveCmd))->setTarget(this);
+ if (inAppDomain) {
+ // Only show these buttons in Options (via Options... in the launcher), and not at game domain level (via Edit Game...)
+ (new GUI::ButtonWidget(widgetsBoss(), "AndroidOptionsDialog.ExportDataButton", _("Export backup"), _("Export a backup of the configuration and save files"), kExportBackupCmd))->setTarget(this);
+ (new GUI::ButtonWidget(widgetsBoss(), "AndroidOptionsDialog.ImportDataButton", _("Import backup"), _("Import a previously exported backup file"), kImportBackupCmd))->setTarget(this);
+ if (AndroidFilesystemFactory::instance().hasSAF()) {
+ // I18N: This button opens a list of all folders added for Android Storage Attached Framework
+ (new GUI::ButtonWidget(widgetsBoss(), "AndroidOptionsDialog.ForgetSAFButton", _("Remove folder authorizations..."), Common::U32String(), kRemoveCmd))->setTarget(this);
+ }
}
}
@@ -234,8 +242,12 @@ void AndroidOptionsWidget::defineLayout(GUI::ThemeEval &layouts, const Common::S
.addWidget("OGames", "PopUp")
.closeLayout();
- if (inAppDomain && AndroidFilesystemFactory::instance().hasSAF()) {
- layouts.addWidget("ForgetSAFButton", "WideButton");
+ if (inAppDomain) {
+ layouts.addWidget("ExportDataButton", "WideButton");
+ layouts.addWidget("ImportDataButton", "WideButton");
+ if (AndroidFilesystemFactory::instance().hasSAF()) {
+ layouts.addWidget("ForgetSAFButton", "WideButton");
+ }
}
layouts.closeLayout()
.closeDialog();
@@ -251,6 +263,60 @@ void AndroidOptionsWidget::handleCommand(GUI::CommandSender *sender, uint32 cmd,
removeDlg.runModal();
break;
}
+ case kExportBackupCmd:
+ {
+ Common::U32String prompt(_("Select backup destination"));
+ int ret = JNI::exportBackup(prompt);
+ if (ret == 1) {
+ // BackupManager.ERROR_CANCELLED
+ break;
+ }
+
+ if (ret == 0 && AndroidFilesystemFactory::instance().hasSAF()) {
+ prompt = _("The backup has been saved successfully.");
+ } else if (ret == 0) {
+ prompt = _("The backup has been saved successfully to the Downloads folder.");
+ } else if (ret == -2) {
+ prompt = _("The game saves couldn't be backed up");
+ } else {
+ prompt = _("An error occured while saving the backup.");
+ }
+ g_system->displayMessageOnOSD(prompt);
+ break;
+ }
+ case kImportBackupCmd:
+ {
+ GUI::MessageDialog alert(_("Restoring a backup will erase the current configuration and overwrite existing saves. Do you want to proceed?"), _("Proceed"), _("Cancel"));
+ if (alert.runModal() != GUI::kMessageOK) {
+ break;
+ }
+
+ Common::U32String prompt(_("Select backup file"));
+ Common::Path path;
+ if (!AndroidFilesystemFactory::instance().hasSAF()) {
+ GUI::BrowserDialog browser(prompt, false);
+ if (browser.runModal() <= 0) {
+ break;
+ }
+
+ path = browser.getResult().getPath();
+ }
+ int ret = JNI::importBackup(prompt, path.toString());
+ if (ret == 1) {
+ // BackupManager.ERROR_CANCELLED
+ break;
+ }
+
+ if (ret == 0) {
+ prompt = _("The backup has been restored successfully.");
+ } else if (ret == -2) {
+ prompt = _("The game saves couldn't be backed up");
+ } else {
+ prompt = _("An error occured while restoring the backup.");
+ }
+ g_system->displayMessageOnOSD(prompt);
+ break;
+ }
default:
GUI::OptionsContainerWidget::handleCommand(sender, cmd, data);
}
diff --git a/backends/platform/android/org/scummvm/scummvm/BackupManager.java b/backends/platform/android/org/scummvm/scummvm/BackupManager.java
new file mode 100644
index 00000000000..4c9d8a978bf
--- /dev/null
+++ b/backends/platform/android/org/scummvm/scummvm/BackupManager.java
@@ -0,0 +1,519 @@
+package org.scummvm.scummvm;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.scummvm.scummvm.zip.ZipFile;
+
+public class BackupManager {
+ public static final int ERROR_CANCELLED = 1;
+ public static final int ERROR_NO_ERROR = 0;
+ public static final int ERROR_INVALID_BACKUP = -1;
+ public static final int ERROR_INVALID_SAVES = -2;
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ public static int exportBackup(Context context, Uri output) {
+ try (ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(output, "wt")) {
+ if (pfd == null) {
+ return ERROR_INVALID_BACKUP;
+ }
+ return exportBackup(context, new FileOutputStream(pfd.getFileDescriptor()));
+ } catch(FileNotFoundException ignored) {
+ return ERROR_INVALID_BACKUP;
+ } catch(IOException ignored) {
+ return ERROR_INVALID_BACKUP;
+ }
+ }
+
+ public static int exportBackup(Context context, File output) {
+ try {
+ return exportBackup(context, new FileOutputStream(output, false));
+ } catch(FileNotFoundException ignored) {
+ return ERROR_INVALID_BACKUP;
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ public static int importBackup(ScummVMActivity context, Uri input) {
+ try (ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(input, "r")) {
+ if (pfd == null) {
+ return ERROR_INVALID_BACKUP;
+ }
+ return importBackup(context, new FileInputStream(pfd.getFileDescriptor()));
+ } catch(FileNotFoundException ignored) {
+ return ERROR_INVALID_BACKUP;
+ } catch(IOException ignored) {
+ return ERROR_INVALID_BACKUP;
+ }
+ }
+
+ public static int importBackup(ScummVMActivity context, File input) {
+ try {
+ return importBackup(context, new FileInputStream(input));
+ } catch(FileNotFoundException ignored) {
+ return ERROR_INVALID_BACKUP;
+ }
+ }
+
+ private static int exportBackup(Context context, FileOutputStream output) {
+ File configuration = new File(context.getFilesDir(), "scummvm.ini");
+
+ Map<String, Map<String, String>> parsedIniMap;
+ try (FileReader reader = new FileReader(configuration)) {
+ parsedIniMap = INIParser.parse(reader);
+ } catch(FileNotFoundException ignored) {
+ parsedIniMap = null;
+ } catch(IOException ignored) {
+ parsedIniMap = null;
+ }
+
+ if (parsedIniMap == null) {
+ return ERROR_INVALID_BACKUP;
+ }
+
+ ZipOutputStream zos = new ZipOutputStream(output);
+
+ try (FileInputStream stream = new FileInputStream(configuration)) {
+ ZipEntry entry = new ZipEntry("scummvm.ini");
+ entry.setSize(configuration.length());
+ entry.setTime(configuration.lastModified());
+
+ zos.putNextEntry(entry);
+ copyStream(zos, stream);
+ zos.closeEntry();
+ } catch(FileNotFoundException ignored) {
+ return ERROR_INVALID_BACKUP;
+ } catch(IOException ignored) {
+ return ERROR_INVALID_BACKUP;
+ }
+
+ try {
+ ZipEntry entry;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ entry = new ZipEntry("saf");
+ zos.putNextEntry(entry);
+ if (!exportTrees(context, zos)) {
+ return ERROR_INVALID_BACKUP;
+ }
+ zos.closeEntry();
+ }
+
+ entry = new ZipEntry("saves/");
+ zos.putNextEntry(entry);
+ zos.closeEntry();
+ } catch(FileNotFoundException ignored) {
+ return ERROR_INVALID_BACKUP;
+ } catch(IOException ignored) {
+ return ERROR_INVALID_BACKUP;
+ }
+
+ File savesPath = INIParser.getPath(parsedIniMap, "scummvm", "savepath",
+ new File(context.getFilesDir(), "saves"));
+ boolean ret = exportSaves(context, savesPath, zos, "saves/");
+ if (!ret) {
+ try {
+ zos.close();
+ } catch(IOException ignored) {
+ }
+ return ERROR_INVALID_BACKUP;
+ }
+
+ HashSet<File> savesPaths = new HashSet<>();
+ savesPaths.add(savesPath);
+
+ int sectionId = -1;
+ for (String section : parsedIniMap.keySet()) {
+ sectionId++;
+ if ("scummvm".equals(section)) {
+ continue;
+ }
+
+ savesPath = INIParser.getPath(parsedIniMap, section, "savepath", null);
+ if (savesPath == null) {
+ continue;
+ }
+
+ if (savesPaths.contains(savesPath)) {
+ continue;
+ }
+ savesPaths.add(savesPath);
+
+ String folderName = "saves-" + sectionId + "/";
+ ZipEntry entry = new ZipEntry(folderName);
+ try {
+ zos.putNextEntry(entry);
+ zos.closeEntry();
+ } catch(IOException ignored) {
+ return ERROR_INVALID_BACKUP;
+ }
+
+ ret = exportSaves(context, savesPath, zos, folderName);
+ if (!ret) {
+ break;
+ }
+ }
+
+ try {
+ zos.close();
+ } catch(IOException ignored) {
+ return ERROR_INVALID_BACKUP;
+ }
+
+ return ret ? ERROR_NO_ERROR : ERROR_INVALID_SAVES;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ static private boolean exportTrees(Context context, ZipOutputStream zos) throws IOException {
+ ObjectOutputStream oos = new ObjectOutputStream(zos);
+ // Version 1
+ oos.writeInt(1);
+
+ SAFFSTree[] trees = SAFFSTree.getTrees(context);
+
+ oos.writeInt(trees.length);
+ for (SAFFSTree tree : trees) {
+ oos.writeObject(tree.getTreeName());
+ oos.writeObject(tree.getTreeId());
+ oos.writeObject(tree.getTreeDocumentUri().toString());
+ }
+
+ // Don't close as it would close the underlying ZipOutputStream
+ oos.flush();
+
+ return true;
+ }
+
+ static private boolean exportSaves(Context context, File folder, ZipOutputStream zos, String folderName) {
+ SAFFSTree.PathResult pr;
+ try {
+ pr = SAFFSTree.fullPathToNode(context, folder.getPath(), false);
+ } catch (FileNotFoundException e) {
+ return false;
+ }
+
+ // This version check is only to make Android Studio linter happy
+ if (pr == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ // This is a standard filesystem path
+ File[] children = folder.listFiles();
+ if (children == null) {
+ return true;
+ }
+ Arrays.sort(children);
+ for (File f: children) {
+ if ("timestamps".equals(f.getName())) {
+ continue;
+ }
+ try (FileInputStream stream = new FileInputStream(f)) {
+ ZipEntry entry = new ZipEntry(folderName + f.getName());
+ entry.setSize(f.length());
+ entry.setTime(f.lastModified());
+
+ zos.putNextEntry(entry);
+ copyStream(zos, stream);
+ zos.closeEntry();
+ } catch(FileNotFoundException ignored) {
+ return false;
+ } catch(IOException ignored) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // This is a SAF fake mount point
+ SAFFSTree.SAFFSNode[] children = pr.tree.getChildren(pr.node);
+ if (children == null) {
+ return false;
+ }
+ Arrays.sort(children);
+
+ for (SAFFSTree.SAFFSNode child : children) {
+ if ((child._flags & SAFFSTree.SAFFSNode.DIRECTORY) != 0) {
+ continue;
+ }
+ String component = child._path.substring(child._path.lastIndexOf('/') + 1);
+ if ("timestamps".equals(component)) {
+ continue;
+ }
+ try (ParcelFileDescriptor pfd = pr.tree.createFileDescriptor(child, "r")) {
+ ZipEntry entry = new ZipEntry(folderName + component);
+
+ zos.putNextEntry(entry);
+ copyStream(zos, new FileInputStream(pfd.getFileDescriptor()));
+ zos.closeEntry();
+ } catch(FileNotFoundException ignored) {
+ return false;
+ } catch(IOException ignored) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static int importBackup(ScummVMActivity context, FileInputStream input) {
+ ZipFile zf;
+ try {
+ zf = new ZipFile(input);
+ } catch(IOException ignored) {
+ return ERROR_INVALID_BACKUP;
+ }
+
+ // Load configuration
+ org.scummvm.scummvm.zip.ZipEntry ze = zf.getEntry("scummvm.ini");
+ if (ze == null) {
+ // No configuration file, not a backup
+ return ERROR_INVALID_BACKUP;
+ }
+
+ // Avoid using tmp suffix as it's used by atomic file support
+ File configurationTmp = new File(context.getFilesDir(), "scummvm.ini.tmp2");
+
+ try (FileOutputStream os = new FileOutputStream(configurationTmp);
+ InputStream is = zf.getInputStream(ze)) {
+ copyStream(os, is);
+ } catch(FileNotFoundException ignored) {
+ return ERROR_INVALID_BACKUP;
+ } catch(IOException ignored) {
+ return ERROR_INVALID_BACKUP;
+ }
+
+ // Load the new configuration to know where to put saves
+ Map<String, Map<String, String>> parsedIniMap;
+ try (FileReader reader = new FileReader(configurationTmp)) {
+ parsedIniMap = INIParser.parse(reader);
+ } catch(FileNotFoundException ignored) {
+ parsedIniMap = null;
+ } catch(IOException ignored) {
+ parsedIniMap = null;
+ }
+
+ if (parsedIniMap == null) {
+ configurationTmp.delete();
+ return ERROR_INVALID_BACKUP;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ // Restore the missing SAF trees
+ ze = zf.getEntry("saf");
+ if (ze == null) {
+ // No configuration file, not a backup
+ return ERROR_INVALID_BACKUP;
+ }
+ try (InputStream is = zf.getInputStream(ze)) {
+ if (importTrees(context, is) == ERROR_INVALID_BACKUP) {
+ // Try to continue except obvious error
+ return ERROR_INVALID_BACKUP;
+ }
+ } catch(FileNotFoundException ignored) {
+ return ERROR_INVALID_BACKUP;
+ } catch(IOException ignored) {
+ return ERROR_INVALID_BACKUP;
+ } catch (ClassNotFoundException e) {
+ return ERROR_INVALID_BACKUP;
+ }
+ }
+
+ // Move the configuration back now that we know it's parsable and that SAF is set up
+ Log.i(ScummVM.LOG_TAG, "Writing new ScummVM configuration");
+ File configuration = new File(context.getFilesDir(), "scummvm.ini");
+ if (!configurationTmp.renameTo(configuration)) {
+ try (FileOutputStream os = new FileOutputStream(configuration);
+ FileInputStream is = new FileInputStream(configurationTmp)) {
+ copyStream(os, is);
+ } catch(FileNotFoundException ignored) {
+ return ERROR_INVALID_BACKUP;
+ } catch(IOException ignored) {
+ return ERROR_INVALID_BACKUP;
+ }
+ configurationTmp.delete();
+ }
+
+ File savesPath = INIParser.getPath(parsedIniMap, "scummvm", "savepath",
+ new File(context.getFilesDir(), "saves"));
+
+ boolean ret = importSaves(context, savesPath, zf, "saves/");
+ if (!ret) {
+ try {
+ zf.close();
+ } catch(IOException ignored) {
+ }
+ return ERROR_INVALID_BACKUP;
+ }
+
+ HashSet<File> savesPaths = new HashSet<>();
+ savesPaths.add(savesPath);
+
+ int sectionId = -1;
+ for (String section : parsedIniMap.keySet()) {
+ sectionId++;
+ if ("scummvm".equals(section)) {
+ continue;
+ }
+
+ savesPath = INIParser.getPath(parsedIniMap, section, "savepath", null);
+ if (savesPath == null) {
+ continue;
+ }
+
+ if (savesPaths.contains(savesPath)) {
+ continue;
+ }
+ savesPaths.add(savesPath);
+
+ String folderName = "saves-" + sectionId + "/";
+ ret = importSaves(context, savesPath, zf, folderName);
+ if (!ret) {
+ break;
+ }
+ }
+
+ try {
+ zf.close();
+ } catch(IOException ignored) {
+ }
+
+ return ret ? ERROR_NO_ERROR : ERROR_INVALID_SAVES;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ static private int importTrees(ScummVMActivity context, InputStream is) throws IOException, ClassNotFoundException {
+ boolean failed = false;
+
+ ObjectInputStream ois = new ObjectInputStream(is);
+ // Version 1
+ if (ois.readInt() != 1) {
+ // Invalid version
+ return ERROR_INVALID_BACKUP;
+ }
+
+ for (int length = ois.readInt(); length > 0; length--) {
+ String treeName = (String)ois.readObject();
+ String treeId = (String)ois.readObject();
+ String treeUri = (String)ois.readObject();
+
+ if (SAFFSTree.findTree(context, treeId) != null) {
+ continue;
+ }
+
+ Uri uri = context.selectWithNativeUI(true, true, Uri.parse(treeUri), treeName, null, null);
+ if (uri == null) {
+ failed = true;
+ continue;
+ }
+
+ // Register the new selected tree
+ SAFFSTree.newTree(context, uri);
+ }
+
+ ois.close();
+
+ return failed ? ERROR_CANCELLED : ERROR_NO_ERROR;
+ }
+
+ static private boolean importSaves(Context context, File folder, ZipFile zf, String folderName) {
+ SAFFSTree.PathResult pr;
+ try {
+ pr = SAFFSTree.fullPathToNode(context, folder.getPath(), true);
+ } catch (FileNotFoundException e) {
+ return false;
+ }
+
+ // This version check is only to make Android Studio linter happy
+ if (pr == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ // This is a standard filesystem path
+ if (!folder.mkdirs()) {
+ return false;
+ }
+
+ Enumeration<? extends org.scummvm.scummvm.zip.ZipEntry> entries = zf.entries();
+ while (entries.hasMoreElements()) {
+ org.scummvm.scummvm.zip.ZipEntry entry = entries.nextElement();
+ String name = entry.getName();
+ if (!name.startsWith(folderName)) {
+ continue;
+ }
+
+ // Get the base name (this avoids directory traversal)
+ name = name.substring(name.lastIndexOf("/") + 1);
+ if (name.isEmpty() || "timestamps".equals(name)) {
+ continue;
+ }
+
+ File f = new File(folder, name);
+ try (InputStream is = zf.getInputStream(entry);
+ FileOutputStream os = new FileOutputStream(f)) {
+ copyStream(os, is);
+ } catch(FileNotFoundException ignored) {
+ return false;
+ } catch(IOException ignored) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // This is a SAF fake mount point
+ Enumeration<? extends org.scummvm.scummvm.zip.ZipEntry> entries = zf.entries();
+ while (entries.hasMoreElements()) {
+ org.scummvm.scummvm.zip.ZipEntry entry = entries.nextElement();
+ String name = entry.getName();
+ if (!name.startsWith(folderName)) {
+ continue;
+ }
+
+ // Get the base name (this avoids directory traversal)
+ name = name.substring(name.lastIndexOf("/") + 1);
+ if (name.isEmpty() || "timestamps".equals(name)) {
+ continue;
+ }
+
+ SAFFSTree.SAFFSNode child = pr.tree.getChild(pr.node, name);
+ if (child == null) {
+ child = pr.tree.createFile(pr.node, name);
+ }
+ if (child == null) {
+ return false;
+ }
+
+ try (InputStream is = zf.getInputStream(entry);
+ ParcelFileDescriptor pfd = pr.tree.createFileDescriptor(child, "wt")) {
+ copyStream(new FileOutputStream(pfd.getFileDescriptor()), is);
+ } catch(FileNotFoundException ignored) {
+ return false;
+ } catch(IOException ignored) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static void copyStream(OutputStream out, InputStream in) throws IOException {
+ byte[] buffer = new byte[4096];
+ int sz;
+ while((sz = in.read(buffer)) != -1) {
+ out.write(buffer, 0, sz);
+ }
+ }
+}
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVM.java b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
index e32087afbef..e66de6faf80 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVM.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
@@ -99,6 +99,8 @@ public abstract class ScummVM implements SurfaceHolder.Callback,
abstract protected SAFFSTree getNewSAFTree(boolean write, String initialURI, String prompt);
abstract protected SAFFSTree[] getSAFTrees();
abstract protected SAFFSTree findSAFTree(String name);
+ abstract protected int exportBackup(String prompt);
+ abstract protected int importBackup(String prompt, String path);
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 b0a4d11d163..317368e0e59 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -5,6 +5,7 @@ import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ClipboardManager;
+import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -19,6 +20,8 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
import android.os.Process;
import android.os.SystemClock;
import android.provider.DocumentsContract;
@@ -53,7 +56,9 @@ import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.text.SimpleDateFormat;
import java.util.Arrays;
+import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -914,6 +919,61 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
protected SAFFSTree findSAFTree(String name) {
return SAFFSTree.findTree(ScummVMActivity.this, name);
}
+
+ @Override
+ protected int exportBackup(String prompt) {
+ String filename = (new SimpleDateFormat("'ScummVM backup 'yyyyMMdd-HHmmss'.zip'")).format(new Date());
+ int ret;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ Uri uri = selectWithNativeUI(false, true, null, prompt, "application/zip", filename);
+ if (uri == null) {
+ return BackupManager.ERROR_CANCELLED;
+ }
+ ret = BackupManager.exportBackup(ScummVMActivity.this, uri);
+ getContentResolver().releasePersistableUriPermission(uri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ } else {
+ File path = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
+ filename);
+ ret = BackupManager.exportBackup(ScummVMActivity.this, path);
+ }
+ return ret;
+ }
+
+ @Override
+ protected int importBackup(String prompt, String path) {
+ int ret;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ Uri uri = selectWithNativeUI(false, false, null, prompt, "application/zip", null);
+ if (uri == null) {
+ return BackupManager.ERROR_CANCELLED;
+ }
+ ret = BackupManager.importBackup(ScummVMActivity.this, uri);
+ getContentResolver().releasePersistableUriPermission(uri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ } else if (path != null) {
+ ret = BackupManager.importBackup(ScummVMActivity.this, new File(path));
+ } else {
+ return BackupManager.ERROR_CANCELLED;
+ }
+ if (ret != BackupManager.ERROR_INVALID_BACKUP) {
+ // Trigger a restart after letting the code return to display the status
+ // This is the same as runOnUiThread but allows to delay the restart
+ new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ Intent restartIntent = Intent.makeRestartActivityTask(new ComponentName(ScummVMActivity.this, SplashActivity.class));
+ restartIntent.setPackage(getPackageName());
+
+ ScummVMActivity.this.startActivity(restartIntent);
+
+ // Kill us to make sure we start from a clean state
+ System.exit(0);
+ }
+ }, 500);
+ }
+ return ret;
+ }
}
private MyScummVM _scummvm;
More information about the Scummvm-git-logs
mailing list