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

lephilousophe noreply at scummvm.org
Sat Jun 1 10:44:23 UTC 2024


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

Summary:
82a3b7fa80 ANDROID: Remove useless leftover from old build system
3bb8fb3ba1 ANDROID: Various fixes to strings translator
a280ccb697 ANDROID: Synchronize Android translations
0c748c894f ANDROID: Use a dedicated properties file for srcdir
030c5afc09 ANDROID: Upgrade build tools and fix deprecation warnings
30c8b3eec3 ANDROID: Use a dedicated INI parser for our configuration file
b3e5cf8e58 ANDROID: Optimize imports
12cf731fb6 ANDROID: Fix style of manifest
497cd57483 ANDROID: Fix deprecation warnings in Manifest
59d4855a92 ANDROID: Disable Jetifier
074344f84e ANDROID: Fix dependencies in port Makefile
4ecf2e4ccc ANDROID: Allow to use an Intent to start a specific game
81669642da ANDROID: Add getPath method to INIParser
1a76acc938 ANDROID: Allow sorting of SAFFSNode using their path component
c1827fd420 ANDROID: Add a method to get a ParcelFileDescriptor from a SAFFSNode
960c74ad6a ANDROID: Add compatibility shims to handle shortcuts and drawables
e70b758496 ANDROID: Add UI to add game shortcuts on the launcher
a329f62069 ANDROID: Add a search field to games list
c966d31c21 ANDROID: Add our own ZipFile implementation
c998525443 ANDROID: Use our own ZipFile implementation and load icons when needed
e0eac792df ANDROID: Fix typo in comment


Commit: 82a3b7fa80b30d3b906459f28d1a6c3f430f48af
    https://github.com/scummvm/scummvm/commit/82a3b7fa80b30d3b906459f28d1a6c3f430f48af
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Remove useless leftover from old build system

Changed paths:
  R dists/android/jni/Android.mk


diff --git a/dists/android/jni/Android.mk b/dists/android/jni/Android.mk
deleted file mode 100644
index 0b3ee4d22e2..00000000000
--- a/dists/android/jni/Android.mk
+++ /dev/null
@@ -1,9 +0,0 @@
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-
-APP_ABI := $(ABI)
-LOCAL_MODULE := scummvm
-LOCAL_SRC_FILES := ../libscummvm.so
-
-include $(PREBUILT_SHARED_LIBRARY)


Commit: 3bb8fb3ba1cf169b29a76e5bb72c507f25e4998c
    https://github.com/scummvm/scummvm/commit/3bb8fb3ba1cf169b29a76e5bb72c507f25e4998c
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Various fixes to strings translator

- Make it independent of current work dir
- Fix languages parsing
- Various stylistic fixes

Changed paths:
    devtools/generate-android-i18n-strings.py


diff --git a/devtools/generate-android-i18n-strings.py b/devtools/generate-android-i18n-strings.py
index a1e119f9675..94d2917a4d5 100644
--- a/devtools/generate-android-i18n-strings.py
+++ b/devtools/generate-android-i18n-strings.py
@@ -15,6 +15,7 @@ import os
 import re
 import xml.etree.ElementTree as ET
 
+BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
 
 def generate_fake_cpp():
     cpp_text = '''/* ScummVM - Graphic Adventure Engine
@@ -46,18 +47,18 @@ def generate_fake_cpp():
  
 #include "common/translation.h" // For catching the file during POTFILES reviews\n
 '''
-    with open('../dists/android.strings.xml.cpp', 'w') as file:
+    with open(os.path.join(BASE_PATH, 'dists/android.strings.xml.cpp'), 'w') as file:
         file.write(cpp_text)
-        tree = ET.parse('../dists/android/res/values/strings.xml')
+        tree = ET.parse(os.path.join(BASE_PATH, 'dists/android/res/values/strings.xml'))
         root = tree.getroot()
         for string in root.findall('string'):
-            if (string.attrib.get("translatable") != "false"):
+            if string.attrib.get('translatable') != 'false':
                 file.write(
                     f'static Common::U32String {string.attrib.get("name")} = _("{string.text}");\n')
 
 
 def extract_translations(file):
-    po_file = polib.pofile('../po/' + file + '.po')
+    po_file = polib.pofile(os.path.join(BASE_PATH, 'po', file + '.po'))
     translations = {}
     for entry in po_file:
         if entry.msgid and entry.msgstr:
@@ -79,30 +80,33 @@ def escape_special_characters(translated_string):
     return escaped
 
 
+def is_regional_language_code(language_code):
+    pattern = r'^[a-zA-Z]{2}([-_][a-zA-Z0-9]{2})?$'
+    return re.match(pattern, language_code)
+
+
 def is_bcp47_language_code(language_code):
-    pattern = r'^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$'
-    if re.match(pattern, language_code):
-        return True
-    else:
-        return False
+    pattern = r'^[a-zA-Z]{1,8}([-_][a-zA-Z0-9]{1,8})*$'
+    return re.match(pattern, language_code)
 
 
 def get_lang_qualifier(file):
     '''Generates <qualifier> for res/values-<qualifier> directory as per the specs given here:
     https://developer.android.com/guide/topics/resources/providing-resources#AlternativeResources
     '''
-    lang_qualifier = file[0] + file[1]
-    if (is_bcp47_language_code(file)):
-        subtags = file.split("-")
-        lang_qualifier = "+".join(subtags)
-        lang_qualifier = "b+" + lang_qualifier
-    else:
+    if is_regional_language_code(file):
         lang_qualifier = file.replace('_', '-r')
+    elif is_bcp47_language_code(file):
+        subtags = re.split('[-_]', file)
+        lang_qualifier = '+'.join(subtags)
+        lang_qualifier = 'b+' + lang_qualifier
+    else:
+        raise Exception(f"Invalid language code: {file}")
     return lang_qualifier
 
 
 def generate_translated_xml(file):
-    tree = ET.parse('../dists/android/res/values/strings.xml')
+    tree = ET.parse(os.path.join(BASE_PATH, 'dists/android/res/values/strings.xml'))
     root = tree.getroot()
 
     translations = extract_translations(file)
@@ -115,23 +119,27 @@ def generate_translated_xml(file):
 
     ET.indent(tree, '  ')
 
-    dir = '../dists/android/res/values-' + get_lang_qualifier(file)
+    dir = os.path.join(BASE_PATH, 'dists/android/res/values-' + get_lang_qualifier(file))
 
     if not os.path.exists(dir):
         os.makedirs(dir)
 
-    tree.write(dir + '/strings.xml', encoding='utf-8', xml_declaration=True)
+    tree.write(os.path.join(dir, 'strings.xml'), encoding='utf-8', xml_declaration=True)
 
 
 def get_po_files():
     po_file_names = []
-    for filename in os.listdir("../po/"):
-        if filename.endswith(".po"):
-            if (filename != "be-tarask.po"):
-                # This skips be-tarask file because there is a bug with be-tarask that gives this compile error:
-                # AAPT: error: failed to deserialize resource table: configuration has invalid locale 'be-'.
-                # See the open issue here: https://issuetracker.google.com/issues/234820481
-                po_file_names.append(os.path.splitext(filename)[0])
+    for filename in os.listdir(os.path.join(BASE_PATH, 'po')):
+        if not filename.endswith('.po'):
+            continue
+
+        # This skips be-tarask file because there is a bug with be-tarask that gives this compile error:
+        # AAPT: error: failed to deserialize resource table: configuration has invalid locale 'be-'.
+        # See the open issue here: https://issuetracker.google.com/issues/234820481
+        if (filename == 'be-tarask.po'):
+            continue
+
+        po_file_names.append(os.path.splitext(filename)[0])
 
     return po_file_names
 


Commit: a280ccb697cde6bc38291e869016fc93182b6e08
    https://github.com/scummvm/scummvm/commit/a280ccb697cde6bc38291e869016fc93182b6e08
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Synchronize Android translations

Changed paths:
  A dists/android/res/values-ar/strings.xml
  A dists/android/res/values-b+zh+Hans/strings.xml
  A dists/android/res/values-b+zh+Hant/strings.xml
  A dists/android/res/values-da/strings.xml
  A dists/android/res/values-el/strings.xml
  A dists/android/res/values-eu/strings.xml
  A dists/android/res/values-he/strings.xml
  A dists/android/res/values-hi/strings.xml
  A dists/android/res/values-ja/strings.xml
  A dists/android/res/values-ka/strings.xml
  A dists/android/res/values-ko/strings.xml
  A dists/android/res/values-tr/strings.xml
  A dists/android/res/values-zh/strings.xml
  R dists/android/res/values-b+da/strings.xml
  R dists/android/res/values-b+el/strings.xml
  R dists/android/res/values-b+eu/strings.xml
  R dists/android/res/values-b+he/strings.xml
  R dists/android/res/values-b+hi/strings.xml
  R dists/android/res/values-b+ja/strings.xml
  R dists/android/res/values-b+ko/strings.xml
  R dists/android/res/values-b+tr/strings.xml
  R dists/android/res/values-b+zh/strings.xml
    dists/android.strings.xml.cpp
    dists/android/res/values-be-rBY/strings.xml
    dists/android/res/values-ca-rES/strings.xml
    dists/android/res/values-cs-rCZ/strings.xml
    dists/android/res/values-de-rDE/strings.xml
    dists/android/res/values-es-rES/strings.xml
    dists/android/res/values-fi-rFI/strings.xml
    dists/android/res/values-fr-rFR/strings.xml
    dists/android/res/values-gl-rES/strings.xml
    dists/android/res/values-hu-rHU/strings.xml
    dists/android/res/values-it-rIT/strings.xml
    dists/android/res/values-nb-rNO/strings.xml
    dists/android/res/values-nl-rNL/strings.xml
    dists/android/res/values-nn-rNO/strings.xml
    dists/android/res/values-pl-rPL/strings.xml
    dists/android/res/values-pt-rBR/strings.xml
    dists/android/res/values-pt-rPT/strings.xml
    dists/android/res/values-ru-rRU/strings.xml
    dists/android/res/values-sv-rSE/strings.xml
    dists/android/res/values-uk-rUA/strings.xml


diff --git a/dists/android.strings.xml.cpp b/dists/android.strings.xml.cpp
index b68ce6468d8..712d1a7223b 100644
--- a/dists/android.strings.xml.cpp
+++ b/dists/android.strings.xml.cpp
@@ -28,9 +28,12 @@
 #include "common/translation.h" // For catching the file during POTFILES reviews
 
 static Common::U32String app_name = _("ScummVM");
+static Common::U32String app_name_debug = _("ScummVM (debug)");
 static Common::U32String app_desc = _("Graphic adventure game engine");
 static Common::U32String ok = _("OK");
 static Common::U32String quit = _("Quit");
+static Common::U32String no_log_file_title = _("Log File Error");
+static Common::U32String no_log_file = _("Unable to read ScummVM log file or create a new one!");
 static Common::U32String no_config_file_title = _("Config File Error");
 static Common::U32String no_config_file = _("Unable to read ScummVM config file or create a new one!");
 static Common::U32String no_save_path_title = _("Save Path Error");
diff --git a/dists/android/res/values-ar/strings.xml b/dists/android/res/values-ar/strings.xml
new file mode 100644
index 00000000000..0f2121d9c5a
--- /dev/null
+++ b/dists/android/res/values-ar/strings.xml
@@ -0,0 +1,16 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+  <string name="app_name">اخرج من ScummVM</string>
+  <string name="ok">حسناً</string>
+  <string name="quit">غادر</string>
+  <string name="no_save_path_title">حفظ مسار:</string>
+  <string name="no_icons_path_title">مسار الرمز:</string>
+  <string name="keyboard_toggle_btn_desc">تبديل وضع السحب</string>
+  <string name="customkeyboardview_keycode_alt">بديل</string>
+  <string name="customkeyboardview_keycode_cancel">إلغاء</string>
+  <string name="customkeyboardview_keycode_delete">حذف</string>
+  <string name="customkeyboardview_keycode_mode_change">تبادل</string>
+  <string name="customkeyboardview_keycode_shift">مفتاح التحول</string>
+  <string name="customkeyboardview_keycode_enter">يدخل</string>
+  <string name="customkeyboardview_popup_close">إغلاق</string>
+</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-b+el/strings.xml b/dists/android/res/values-b+el/strings.xml
deleted file mode 100644
index d4400be05c7..00000000000
--- a/dists/android/res/values-b+el/strings.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version='1.0' encoding='utf-8'?>
-<resources>
-  <string name="ok">OK</string>
-  <string name="quit">Έξοδος</string>
-  <string name="customkeyboardview_keycode_alt">Alt</string>
-  <string name="customkeyboardview_keycode_cancel">Ακύρωση</string>
-  <string name="customkeyboardview_keycode_delete">Σβήσιμο</string>
-  <string name="customkeyboardview_keycode_shift">Πλήκτρο Shift</string>
-  <string name="customkeyboardview_keycode_enter">Enter</string>
-</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-b+eu/strings.xml b/dists/android/res/values-b+eu/strings.xml
deleted file mode 100644
index fd9c33a4cdc..00000000000
--- a/dists/android/res/values-b+eu/strings.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version='1.0' encoding='utf-8'?>
-<resources>
-  <string name="ok">Ados</string>
-  <string name="quit">Irten</string>
-  <string name="customkeyboardview_keycode_alt">Alt</string>
-  <string name="customkeyboardview_keycode_cancel">Utzi</string>
-  <string name="customkeyboardview_keycode_delete">Ezabatu</string>
-  <string name="customkeyboardview_keycode_enter">Sartu</string>
-</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-b+he/strings.xml b/dists/android/res/values-b+he/strings.xml
deleted file mode 100644
index e6a7e620030..00000000000
--- a/dists/android/res/values-b+he/strings.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version='1.0' encoding='utf-8'?>
-<resources>
-  <string name="ok">אישור</string>
-  <string name="quit">יציאה</string>
-  <string name="customkeyboardview_keycode_alt">אלט</string>
-  <string name="customkeyboardview_keycode_cancel">ביטול</string>
-  <string name="customkeyboardview_keycode_delete">מחיקה</string>
-  <string name="customkeyboardview_keycode_shift">Shift</string>
-  <string name="customkeyboardview_keycode_enter">להיכנס</string>
-</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-b+hi/strings.xml b/dists/android/res/values-b+hi/strings.xml
deleted file mode 100644
index 005c84fc6ff..00000000000
--- a/dists/android/res/values-b+hi/strings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version='1.0' encoding='utf-8'?>
-<resources>
-  <string name="ok">ठीक है</string>
-  <string name="quit">खेल</string>
-  <string name="customkeyboardview_keycode_cancel">रद्द करें</string>
-</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-b+ja/strings.xml b/dists/android/res/values-b+ja/strings.xml
deleted file mode 100644
index 42448c1e643..00000000000
--- a/dists/android/res/values-b+ja/strings.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version='1.0' encoding='utf-8'?>
-<resources>
-  <string name="ok">OK</string>
-  <string name="quit">終了</string>
-  <string name="customkeyboardview_keycode_alt">Alt</string>
-  <string name="customkeyboardview_keycode_cancel">キャンセル</string>
-  <string name="customkeyboardview_keycode_delete">削除</string>
-  <string name="customkeyboardview_keycode_shift">シフト</string>
-  <string name="customkeyboardview_keycode_enter">Enter</string>
-</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-b+zh+Hans/strings.xml b/dists/android/res/values-b+zh+Hans/strings.xml
new file mode 100644
index 00000000000..eac3d690987
--- /dev/null
+++ b/dists/android/res/values-b+zh+Hans/strings.xml
@@ -0,0 +1,25 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">图形冒险游戏引擎</string>
+  <string name="ok">确认</string>
+  <string name="quit">退出</string>
+  <string name="no_config_file_title">配置文件错误</string>
+  <string name="no_config_file">无法读取 ScummVM 配置文件,请考虑新建一个!</string>
+  <string name="no_save_path_title">保存路径错误</string>
+  <string name="no_save_path_configured">无法创建或访问默认存档目录!</string>
+  <string name="no_icons_path_title">图标路径错误</string>
+  <string name="no_icons_path_configured">无法创建或访问默认图标与着色器目录!</string>
+  <string name="bad_explicit_save_path_configured">无法访问全局存档目录!请在 ScummVM 设置中恢复到默认值</string>
+  <string name="keyboard_toggle_btn_desc">切换虚拟键盘</string>
+  <string name="customkeyboardview_keycode_alt">Alt</string>
+  <string name="customkeyboardview_keycode_cancel">取消</string>
+  <string name="customkeyboardview_keycode_delete">删除</string>
+  <string name="customkeyboardview_keycode_done">完成</string>
+  <string name="customkeyboardview_keycode_mode_change">模式更改</string>
+  <string name="customkeyboardview_keycode_shift">Shift</string>
+  <string name="customkeyboardview_keycode_enter">回车</string>
+  <string name="customkeyboardview_popup_close">关闭弹出窗口</string>
+  <string name="saf_request_prompt">请选择外部(物理)SD 卡的 *根目录*。ScummVM 需要此目录以访问 SD 卡: </string>
+  <string name="saf_revoke_done">ScummVM 存储访问框架的权限被撤回了!</string>
+</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-b+zh+Hant/strings.xml b/dists/android/res/values-b+zh+Hant/strings.xml
new file mode 100644
index 00000000000..ae1c482c759
--- /dev/null
+++ b/dists/android/res/values-b+zh+Hant/strings.xml
@@ -0,0 +1,3 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+	</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-b+zh/strings.xml b/dists/android/res/values-b+zh/strings.xml
deleted file mode 100644
index f1e9e0474c5..00000000000
--- a/dists/android/res/values-b+zh/strings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version='1.0' encoding='utf-8'?>
-<resources>
-  <string name="ok">确认</string>
-  <string name="quit">離開</string>
-  <string name="customkeyboardview_keycode_cancel">取消</string>
-</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-be-rBY/strings.xml b/dists/android/res/values-be-rBY/strings.xml
index a5efc50ec24..2364b9539c9 100644
--- a/dists/android/res/values-be-rBY/strings.xml
+++ b/dists/android/res/values-be-rBY/strings.xml
@@ -1,10 +1,15 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">Завяршыць ScummVM</string>
   <string name="ok">OK</string>
   <string name="quit">Выхад</string>
+  <string name="no_save_path_title">Захаванні гульняў:</string>
+  <string name="no_icons_path_title">Каранёвая дырэкторыя:</string>
+  <string name="keyboard_toggle_btn_desc">Віртуальная клавіятура</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Адмена</string>
   <string name="customkeyboardview_keycode_delete">Выдаліць</string>
   <string name="customkeyboardview_keycode_shift">Shift</string>
   <string name="customkeyboardview_keycode_enter">Увод</string>
+  <string name="customkeyboardview_popup_close">Закрыць</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-ca-rES/strings.xml b/dists/android/res/values-ca-rES/strings.xml
index 77623064363..23ae79b7546 100644
--- a/dists/android/res/values-ca-rES/strings.xml
+++ b/dists/android/res/values-ca-rES/strings.xml
@@ -1,9 +1,25 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Motor de jocs d\'aventura gràfica</string>
   <string name="ok">D\'acord</string>
   <string name="quit">Surt</string>
+  <string name="no_config_file_title">Error en el fitxer de configuració</string>
+  <string name="no_config_file">No s\'ha pogut llegir el fitxer de configuració de ScummVM o crear-ne un de nou!</string>
+  <string name="no_save_path_title">Error en el camí de les partides desades</string>
+  <string name="no_save_path_configured">No s\'ha pogut crear o accedir a la ruta predeterminada de les partides desades!</string>
+  <string name="no_icons_path_title">Error en el camí de les icones</string>
+  <string name="no_icons_path_configured">No s\'han pogut crear o accedir a la ruta predeterminada de les icones i els shaders!</string>
+  <string name="bad_explicit_save_path_configured">No es pot accedir a la ruta de desament establerta globalment! Torneu al valor predeterminat des de les opcions de ScummVM</string>
+  <string name="keyboard_toggle_btn_desc">Commuta el teclat virtual</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Cancel·la</string>
   <string name="customkeyboardview_keycode_delete">Suprimeix</string>
+  <string name="customkeyboardview_keycode_done">Fet</string>
+  <string name="customkeyboardview_keycode_mode_change">Canvi de mode</string>
+  <string name="customkeyboardview_keycode_shift">Majúscules</string>
   <string name="customkeyboardview_keycode_enter">Intro</string>
+  <string name="customkeyboardview_popup_close">Tanca la finestra emergent</string>
+  <string name="saf_request_prompt">Seleccioneu l\'arrel de la targeta SD externa (física). És necessari perquè ScummVM accedeixi a aquest camí: </string>
+  <string name="saf_revoke_done">S\'han revocat els permisos del framework d\'accés a l\'emmagatzematge per a ScummVM!</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-cs-rCZ/strings.xml b/dists/android/res/values-cs-rCZ/strings.xml
index f7cd2d02049..640c3f90026 100644
--- a/dists/android/res/values-cs-rCZ/strings.xml
+++ b/dists/android/res/values-cs-rCZ/strings.xml
@@ -1,9 +1,14 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">Ukončit ScummVM</string>
   <string name="ok">OK</string>
   <string name="quit">Ukončit</string>
+  <string name="no_save_path_title">Cesta pro uložení:</string>
+  <string name="no_icons_path_title">Cesta /root/:</string>
+  <string name="keyboard_toggle_btn_desc">Virtuální klávesnice</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Zrušit</string>
   <string name="customkeyboardview_keycode_delete">Smazat</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Zavřít</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-b+da/strings.xml b/dists/android/res/values-da/strings.xml
similarity index 55%
rename from dists/android/res/values-b+da/strings.xml
rename to dists/android/res/values-da/strings.xml
index 35d0c4ae6d8..01d241cd95a 100644
--- a/dists/android/res/values-b+da/strings.xml
+++ b/dists/android/res/values-da/strings.xml
@@ -1,9 +1,14 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">Afslut ScummVM</string>
   <string name="ok">OK</string>
   <string name="quit">Afslut</string>
+  <string name="no_save_path_title">Gemmesti:</string>
+  <string name="no_icons_path_title">/root/-sti:</string>
+  <string name="keyboard_toggle_btn_desc">Virtuelt tastatur</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Annuller</string>
   <string name="customkeyboardview_keycode_delete">Slet</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Luk</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-de-rDE/strings.xml b/dists/android/res/values-de-rDE/strings.xml
index 13e9ad9299c..38d467aff7c 100644
--- a/dists/android/res/values-de-rDE/strings.xml
+++ b/dists/android/res/values-de-rDE/strings.xml
@@ -1,10 +1,25 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Spiele-Engine für Grafik-Adventures</string>
   <string name="ok">OK</string>
   <string name="quit">Beenden</string>
+  <string name="no_config_file_title">Fehler in Konfigurationsdatei</string>
+  <string name="no_config_file">Fehler beim Lesen oder Erstellen der ScummVM-Konfigurationsdatei!</string>
+  <string name="no_save_path_title">Speicherpfad-Fehler</string>
+  <string name="no_save_path_configured">Fehler beim Erzeugen oder Zugriff auf den Speicherpfad!</string>
+  <string name="no_icons_path_title">Iconpfad-Fehler</string>
+  <string name="no_icons_path_configured">Fehler beim Erzeugen oder Zugriff auf den Icon- und Shader-Pfad!</string>
+  <string name="bad_explicit_save_path_configured">Auf den global gesetzten Speicherpfad konnte nicht zugegriffen werden! Bitte nutze die ScummVM-Optionen, um auf die Standard-Einstellung zurückzugekehren</string>
+  <string name="keyboard_toggle_btn_desc">Virtuelle Tastatur umschalten</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Abbrechen</string>
   <string name="customkeyboardview_keycode_delete">Löschen</string>
+  <string name="customkeyboardview_keycode_done">Fertig</string>
+  <string name="customkeyboardview_keycode_mode_change">Modus ändern</string>
   <string name="customkeyboardview_keycode_shift">Umschalt-Taste</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Popup schließen</string>
+  <string name="saf_request_prompt">Bitte wähle das *Stammverzeichnis* deiner externen SD-Karte. Dies wird von ScummVM für den Zugriff auf folgenden Pfad benötigt: </string>
+  <string name="saf_revoke_done">Die Storage Access Framework Permissions für ScummVM wurden widerrufen!</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-el/strings.xml b/dists/android/res/values-el/strings.xml
new file mode 100644
index 00000000000..be945f032c7
--- /dev/null
+++ b/dists/android/res/values-el/strings.xml
@@ -0,0 +1,25 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Μηχανή παιχνιδιών περιπέτειας</string>
+  <string name="ok">OK</string>
+  <string name="quit">Έξοδος</string>
+  <string name="no_config_file_title">Σφάλμα Αρχείου Ρυθμίσεων</string>
+  <string name="no_config_file">Αδυναμία ανάγνωσης του αρχείου ρυθμίσεων του ScummVM ή δημιουργίας νέου!</string>
+  <string name="no_save_path_title">Σφάλμα Διαδρομής Αποθήκευσης</string>
+  <string name="no_save_path_configured">Αδυναμία δημιουργίας ή πρόσβασης στην προεπιλεγμένη διαδρομή αποθήκευσης!</string>
+  <string name="no_icons_path_title">Σφάλμα Διαδρομής Εικονιδίων</string>
+  <string name="no_icons_path_configured">Αδυναμία δημιουργίας ή πρόσβασης στην προεπιλεγμένη διαδρομή εικονιδίων και σκιαστών!</string>
+  <string name="bad_explicit_save_path_configured">Αδυναμία πρόσβασης στη διαδρομή αποθήκευσης που έχει τεθεί στις καθολικές ρυθμίσεις! Παρακαλούμε να επαναφέρετε την προεπιλογή από της Ρυθμίσεις του ScummVM</string>
+  <string name="keyboard_toggle_btn_desc">Εναλλαγή εμφάνισης εικονικού πληκτρολογίου</string>
+  <string name="customkeyboardview_keycode_alt">Alt</string>
+  <string name="customkeyboardview_keycode_cancel">Ακύρωση</string>
+  <string name="customkeyboardview_keycode_delete">Σβήσιμο</string>
+  <string name="customkeyboardview_keycode_done">Έγινε</string>
+  <string name="customkeyboardview_keycode_mode_change">Αλλαγή κατάστασης λειτουργίας</string>
+  <string name="customkeyboardview_keycode_shift">Πλήκτρο Shift</string>
+  <string name="customkeyboardview_keycode_enter">Πλήκτρο Enter</string>
+  <string name="customkeyboardview_popup_close">Κλείσιμο αναδυόμενου παραθύρου</string>
+  <string name="saf_request_prompt">Παρακαλούμε, επιλέξετε τον *αρχικό κατάλογο* (root) της εξωτερικής (φυσικής) SD κάρτας σας. Αυτό απαιτείται ώστε να έχει το ScummVM πρόσβαση σε αυτή τη διαδρομή: </string>
+  <string name="saf_revoke_done">Τα Δικαιώματα για το Πλαίσιο Πρόσβασης Αποθηκευτικού Χώρου (Storage Access Framework) για το ScummVM ανακλήθηκαν!</string>
+</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-es-rES/strings.xml b/dists/android/res/values-es-rES/strings.xml
index 2b2db4d6e19..c9806b98051 100644
--- a/dists/android/res/values-es-rES/strings.xml
+++ b/dists/android/res/values-es-rES/strings.xml
@@ -1,10 +1,25 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Motor de aventuras gráficas</string>
   <string name="ok">Aceptar</string>
   <string name="quit">Salir</string>
+  <string name="no_config_file_title">Error en el archivo de configuración</string>
+  <string name="no_config_file">¡No se ha podido leer o crear el archivo de configuración de ScummVM!</string>
+  <string name="no_save_path_title">Error de la ruta de partidas guardadas</string>
+  <string name="no_save_path_configured">¡No se ha podido crear o acceder a la ruta predeterminada de partidas guardadas!</string>
+  <string name="no_icons_path_title">Error de la ruta de iconos</string>
+  <string name="no_icons_path_configured">¡No se ha podido crear o acceder a la ruta predeterminada de iconos y shaders!</string>
+  <string name="bad_explicit_save_path_configured">¡No se ha podido acceder a la ruta global de partidas guardadas! Cámbiala a la predeterminada dentro de las opciones de ScummVM</string>
+  <string name="keyboard_toggle_btn_desc">Alternar teclado virtual</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Cancelar</string>
   <string name="customkeyboardview_keycode_delete">Eliminar</string>
+  <string name="customkeyboardview_keycode_done">Terminar</string>
+  <string name="customkeyboardview_keycode_mode_change">Cambiar modo</string>
   <string name="customkeyboardview_keycode_shift">Mayúsculas</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Cerrar ventana</string>
+  <string name="saf_request_prompt">Selecciona la *raíz* de tu tarjeta SD externa (física). Debes indicarla para que ScummVM pueda acceder a la siguiente ruta: </string>
+  <string name="saf_revoke_done">¡Se han revocado los permisos del framework de acceso al almacenamiento para ScummVM!</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-eu/strings.xml b/dists/android/res/values-eu/strings.xml
new file mode 100644
index 00000000000..af8dc526a04
--- /dev/null
+++ b/dists/android/res/values-eu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Abendura grafikoen motorea</string>
+  <string name="ok">Ados</string>
+  <string name="quit">Irten</string>
+  <string name="no_config_file_title">Konfig Fitxategi Errorea</string>
+  <string name="no_config_file">Ezin izan da ScummVM konfig fitxategia irakurri edo berria sortu!</string>
+  <string name="no_save_path_title">Gordetakoen Bide Errorea:</string>
+  <string name="no_save_path_configured">Ezin izan da gordetze bidea sortu edo atzitu!</string>
+  <string name="no_icons_path_title">Ikono Bide Errorea</string>
+  <string name="no_icons_path_configured">Ezin izan da ikono eta shader-en bidea sortu edo atzitu!</string>
+  <string name="bad_explicit_save_path_configured">Ezin izan da gordetze bide orokorra atzitu! Itzuli defektuzko ScummVM Aukeretara, mesedez</string>
+  <string name="keyboard_toggle_btn_desc">Teklatu birtuala erakutsi/gorde</string>
+  <string name="customkeyboardview_keycode_alt">Alt</string>
+  <string name="customkeyboardview_keycode_cancel">Utzi</string>
+  <string name="customkeyboardview_keycode_delete">Ezabatu</string>
+  <string name="customkeyboardview_keycode_done">Eginda</string>
+  <string name="customkeyboardview_keycode_mode_change">Modua aldatu</string>
+  <string name="customkeyboardview_keycode_shift">Shift</string>
+  <string name="customkeyboardview_keycode_enter">Sartu</string>
+  <string name="customkeyboardview_popup_close">Itxi laster-leihoa</string>
+  <string name="saf_request_prompt">Mesedez aukeratu zure kanpo SD txartelaren *erroa*. Hau ScummVMek bidea atzitzeko beharrezkoa da: </string>
+  <string name="saf_revoke_done">ScummVMrentzako Biltegiratze Atzitze Baimena ukatu egin da!</string>
+</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-fi-rFI/strings.xml b/dists/android/res/values-fi-rFI/strings.xml
index 9cfde692bab..c6640c43ad9 100644
--- a/dists/android/res/values-fi-rFI/strings.xml
+++ b/dists/android/res/values-fi-rFI/strings.xml
@@ -1,10 +1,25 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Pelimoottori graafisille seikkailupeleille</string>
   <string name="ok">OK</string>
   <string name="quit">Lopeta</string>
+  <string name="no_config_file_title">Asetustiedostovirhe</string>
+  <string name="no_config_file">ScummVM:n asetustiedoston lukeminen tai uuden luominen ei onnistu!</string>
+  <string name="no_save_path_title">Tallennuspolkuvirhe</string>
+  <string name="no_save_path_configured">Oletustallennuspolun luominen tai lukeminen ei onnistu!</string>
+  <string name="no_icons_path_title">Ikonipolkuvirhe</string>
+  <string name="no_icons_path_configured">Oletus ikoni- ja sävytinpolun luominen tai lukeminen ei onnistu!</string>
+  <string name="bad_explicit_save_path_configured">Globaalisti asetetun tallennuspolun lukeminen ei onnistu! Ole hyvä ja palauta oletusarvo käyttöön ScummVM:n asetuksista</string>
+  <string name="keyboard_toggle_btn_desc">Virtuaalinen näppäimistö päälle/pois</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Peruuta</string>
   <string name="customkeyboardview_keycode_delete">Poista</string>
+  <string name="customkeyboardview_keycode_done">Valmis</string>
+  <string name="customkeyboardview_keycode_mode_change">Tilan vaihto</string>
   <string name="customkeyboardview_keycode_shift">Shift</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Sulje ponnahdusikkuna</string>
+  <string name="saf_request_prompt">Ole hyvä ja valitse ulkoisen (fyysisen) SD-korttisi *juurihakemisto*. Tämä on tarpeellista jotta ScummVM pääsee kansioon: </string>
+  <string name="saf_revoke_done">ScummVM:n Storage Access Framework-luvat poistettu!</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-fr-rFR/strings.xml b/dists/android/res/values-fr-rFR/strings.xml
index d8785308209..27156042fe0 100644
--- a/dists/android/res/values-fr-rFR/strings.xml
+++ b/dists/android/res/values-fr-rFR/strings.xml
@@ -1,10 +1,25 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Moteur de jeux d\'aventure graphiques</string>
   <string name="ok">OK</string>
   <string name="quit">Quitter</string>
+  <string name="no_config_file_title">Erreur de fichier de configuration</string>
+  <string name="no_config_file">Impossible de lire le fichier de configuration de ScummVM ou d\'en créer un nouveau !</string>
+  <string name="no_save_path_title">Erreur de chemin de sauvegarde</string>
+  <string name="no_save_path_configured">Impossible de créer ou d\'accéder au chemin de sauvegarde par défaut !</string>
+  <string name="no_icons_path_title">Erreur de chemin d\'icônes</string>
+  <string name="no_icons_path_configured">Impossible de créer ou d\'accéder au chemin d\'icônes et de shadders par défaut !</string>
+  <string name="bad_explicit_save_path_configured">Impossible d\'accéder au chemin de sauvegarde défini globalement ! Veuillez rétablir le chemin par défaut à partir des options de ScummVM</string>
+  <string name="keyboard_toggle_btn_desc">Act./Désact. Clavier virtuel</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Annuler</string>
   <string name="customkeyboardview_keycode_delete">Supprimer</string>
+  <string name="customkeyboardview_keycode_done">Terminé</string>
+  <string name="customkeyboardview_keycode_mode_change">Changement de mode</string>
   <string name="customkeyboardview_keycode_shift">Shift</string>
   <string name="customkeyboardview_keycode_enter">Entrer</string>
+  <string name="customkeyboardview_popup_close">Fermer popup</string>
+  <string name="saf_request_prompt">Veuillez sélectionner la *racine* de votre carte SD externe (physique). Cette sélection est nécessaire pour que ScummVM puisse accéder à ce chemin : </string>
+  <string name="saf_revoke_done">Les permissions de Storage Access Framework pour ScummVM ont été révoquées !</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-gl-rES/strings.xml b/dists/android/res/values-gl-rES/strings.xml
index 462a669f762..7f41b4f86e1 100644
--- a/dists/android/res/values-gl-rES/strings.xml
+++ b/dists/android/res/values-gl-rES/strings.xml
@@ -1,9 +1,14 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">Saír de ScummVM</string>
   <string name="ok">Aceptar</string>
   <string name="quit">Saír</string>
+  <string name="no_save_path_title">Camiño de gardado:</string>
+  <string name="no_icons_path_title">Camiño de /root/:</string>
+  <string name="keyboard_toggle_btn_desc">Teclado virtual</string>
   <string name="customkeyboardview_keycode_alt">ALT</string>
   <string name="customkeyboardview_keycode_cancel">Cancelar</string>
   <string name="customkeyboardview_keycode_delete">Eliminar</string>
   <string name="customkeyboardview_keycode_enter">INTRO</string>
+  <string name="customkeyboardview_popup_close">Pechar</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-he/strings.xml b/dists/android/res/values-he/strings.xml
new file mode 100644
index 00000000000..940dcef0b17
--- /dev/null
+++ b/dists/android/res/values-he/strings.xml
@@ -0,0 +1,21 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">מנוע משחק הרפתקה גרפי</string>
+  <string name="ok">אישור</string>
+  <string name="quit">יציאה</string>
+  <string name="no_config_file_title">שגיאת קובץ תצורה</string>
+  <string name="no_config_file">לא ניתן לקרוא את קובץ התצורה של ScummVM או ליצור חדש!</string>
+  <string name="no_save_path_title">שגיאת נתיב שמירה</string>
+  <string name="no_save_path_configured">לא ניתן ליצור נתיב שמירה ברירת מחדל או לגשת אליו!</string>
+  <string name="no_icons_path_title">שגיאת נתיב צלמיות</string>
+  <string name="no_icons_path_configured">לא ניתן ליצור נתיב ברירת מחדל עבור צלמיות וסוככים או לגשת אליו!</string>
+  <string name="keyboard_toggle_btn_desc">מיתוג מקלדת וירטואלית</string>
+  <string name="customkeyboardview_keycode_alt">אלט</string>
+  <string name="customkeyboardview_keycode_cancel">ביטול</string>
+  <string name="customkeyboardview_keycode_delete">מחיקה</string>
+  <string name="customkeyboardview_keycode_mode_change">שינוי מצב</string>
+  <string name="customkeyboardview_keycode_shift">Shift</string>
+  <string name="customkeyboardview_keycode_enter">להיכנס</string>
+  <string name="customkeyboardview_popup_close">סגירת חלון קופץ</string>
+</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-hi/strings.xml b/dists/android/res/values-hi/strings.xml
new file mode 100644
index 00000000000..b9442c75aa7
--- /dev/null
+++ b/dists/android/res/values-hi/strings.xml
@@ -0,0 +1,9 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+  <string name="app_name">ScummVM बंद करें</string>
+  <string name="ok">ठीक है</string>
+  <string name="quit">अंत करे</string>
+  <string name="no_save_path_title">सेव पाथ:</string>
+  <string name="customkeyboardview_keycode_cancel">रद्द करें</string>
+  <string name="customkeyboardview_popup_close">बंद करें</string>
+</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-hu-rHU/strings.xml b/dists/android/res/values-hu-rHU/strings.xml
index c5badc466a5..19e307a0043 100644
--- a/dists/android/res/values-hu-rHU/strings.xml
+++ b/dists/android/res/values-hu-rHU/strings.xml
@@ -1,10 +1,23 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Grafikus kalandjáték motor</string>
   <string name="ok">OK</string>
   <string name="quit">Kilépés</string>
+  <string name="no_config_file_title">Konfigurációs fájl hiba</string>
+  <string name="no_config_file">Nem lehet olvasni a ScummVM konfigurációs fájlt, vagy nem lehet újat létrehozni!</string>
+  <string name="no_save_path_title">Mentési útvonal hiba</string>
+  <string name="no_save_path_configured">Nem lehet létrehozni vagy elérni az alapértelmezett mentési útvonalat!</string>
+  <string name="no_icons_path_title">Ikon útvonal hiba</string>
+  <string name="no_icons_path_configured">Nem lehet létrehozni vagy elérni az alapértelmezett ikonokat és árnyékolók elérési útját!</string>
+  <string name="bad_explicit_save_path_configured">Nem érhető el a globálisan beállított mentési útvonal! Kérjük, állítsa vissza az alapértelmezett beállításokat a ScummVM beállítások közül</string>
+  <string name="keyboard_toggle_btn_desc">Virtuális billentyűzet váltása</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Mégse</string>
   <string name="customkeyboardview_keycode_delete">Töröl</string>
+  <string name="customkeyboardview_keycode_done">Kész</string>
+  <string name="customkeyboardview_keycode_mode_change">Mód csere</string>
   <string name="customkeyboardview_keycode_shift">Shift</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Felugró ablak bezárása</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-it-rIT/strings.xml b/dists/android/res/values-it-rIT/strings.xml
index 9871d2153f3..b3cc764dba1 100644
--- a/dists/android/res/values-it-rIT/strings.xml
+++ b/dists/android/res/values-it-rIT/strings.xml
@@ -1,10 +1,25 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Motore per avventure grafiche</string>
   <string name="ok">OK</string>
   <string name="quit">Esci</string>
+  <string name="no_config_file_title">Errore nel file di configurazione</string>
+  <string name="no_config_file">Non è stato possibile leggere o creare il file di configurazione di ScummVM!</string>
+  <string name="no_save_path_title">Errore percorso salvataggi</string>
+  <string name="no_save_path_configured">Non è stato possibile creare o accedere al percorso dei salvataggi predefinito!</string>
+  <string name="no_icons_path_title">Errore percorso icone</string>
+  <string name="no_icons_path_configured">Non è stato possibile creare o accedere al percorso predefinito per icone e shader!</string>
+  <string name="bad_explicit_save_path_configured">Non è stato possibile accedere al percorso salvataggi globale! Ripristina il percorso predefinito nelle Opzioni di ScummVM</string>
+  <string name="keyboard_toggle_btn_desc">Mostra/nascondi tastiera virtuale</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Annulla</string>
   <string name="customkeyboardview_keycode_delete">Elimina</string>
+  <string name="customkeyboardview_keycode_done">Fatto</string>
+  <string name="customkeyboardview_keycode_mode_change">Cambia modalità</string>
   <string name="customkeyboardview_keycode_shift">Tasto Maiusc</string>
   <string name="customkeyboardview_keycode_enter">Invio</string>
+  <string name="customkeyboardview_popup_close">Chiudi popup</string>
+  <string name="saf_request_prompt">Per favore, seleziona la *root* della tua scheda esterna SD (fisica). È necessario affinchè ScummVM possa accedere al percorso: </string>
+  <string name="saf_revoke_done">Permessi Storage Access Framework per ScummVM revocati!</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-ja/strings.xml b/dists/android/res/values-ja/strings.xml
new file mode 100644
index 00000000000..fd71378c352
--- /dev/null
+++ b/dists/android/res/values-ja/strings.xml
@@ -0,0 +1,25 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">グラフィックアドベンチャーエンジン</string>
+  <string name="ok">OK</string>
+  <string name="quit">終了</string>
+  <string name="no_config_file_title">設定ファイルエラー</string>
+  <string name="no_config_file">ScummVM設定ファイルの読込/作成ができません!</string>
+  <string name="no_save_path_title">セーブパスエラー</string>
+  <string name="no_save_path_configured">デフォルトのセーブフォルダーにアクセス/作成できません!</string>
+  <string name="no_icons_path_title">アイコンパスエラー</string>
+  <string name="no_icons_path_configured">デフォルトのアイコンパスにアクセス/作成できません!</string>
+  <string name="bad_explicit_save_path_configured">グローバルで設定されたセーブフォルダーにアクセスできません! リセットして下さい</string>
+  <string name="keyboard_toggle_btn_desc">仮想キーボードの切替</string>
+  <string name="customkeyboardview_keycode_alt">Alt</string>
+  <string name="customkeyboardview_keycode_cancel">キャンセル</string>
+  <string name="customkeyboardview_keycode_delete">削除</string>
+  <string name="customkeyboardview_keycode_done">実行</string>
+  <string name="customkeyboardview_keycode_mode_change">モード切替</string>
+  <string name="customkeyboardview_keycode_shift">シフト</string>
+  <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">ポップアップを閉じる</string>
+  <string name="saf_request_prompt">外付け(物理)SDカードのルートを選択して下さい。ScummVMがアクセスするために必要です: </string>
+  <string name="saf_revoke_done">ScummVMのStorage Access Frameworkの権限が取り消されました!</string>
+</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-ka/strings.xml b/dists/android/res/values-ka/strings.xml
new file mode 100644
index 00000000000..b8aab1a8a46
--- /dev/null
+++ b/dists/android/res/values-ka/strings.xml
@@ -0,0 +1,18 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+  <string name="app_name">ScummVM</string>
+  <string name="ok">დიახ</string>
+  <string name="quit">გასვლა</string>
+  <string name="no_config_file_title">კონფიგურაციის ფაილის სეცდომა</string>
+  <string name="no_save_path_title">ბილიკის შენახვის შეცდომა</string>
+  <string name="no_icons_path_title">ხატულების ბილიკის შეცდომა</string>
+  <string name="keyboard_toggle_btn_desc">ვირტუალური კლავიატურის გადართვა</string>
+  <string name="customkeyboardview_keycode_alt">Alt</string>
+  <string name="customkeyboardview_keycode_cancel">გაუქმება</string>
+  <string name="customkeyboardview_keycode_delete">წაშლა</string>
+  <string name="customkeyboardview_keycode_done">დასრულდა</string>
+  <string name="customkeyboardview_keycode_mode_change">რეჟიმის შეცვლა</string>
+  <string name="customkeyboardview_keycode_shift">Shift</string>
+  <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">მხტუნარას დახურვა</string>
+</resources>
\ No newline at end of file
diff --git a/dists/android/res/values-b+ko/strings.xml b/dists/android/res/values-ko/strings.xml
similarity index 59%
rename from dists/android/res/values-b+ko/strings.xml
rename to dists/android/res/values-ko/strings.xml
index 7b96277ce1c..f557871732e 100644
--- a/dists/android/res/values-b+ko/strings.xml
+++ b/dists/android/res/values-ko/strings.xml
@@ -1,10 +1,15 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM 종료</string>
   <string name="ok">확인</string>
   <string name="quit">종료</string>
+  <string name="no_save_path_title">저장 경로:</string>
+  <string name="no_icons_path_title">/icon/ 경로:</string>
+  <string name="keyboard_toggle_btn_desc">가상 키보드</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">취소</string>
   <string name="customkeyboardview_keycode_delete">삭제</string>
   <string name="customkeyboardview_keycode_shift">시프트 키</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">닫기</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-nb-rNO/strings.xml b/dists/android/res/values-nb-rNO/strings.xml
index 7c49bb9afb7..7ff53b90182 100644
--- a/dists/android/res/values-nb-rNO/strings.xml
+++ b/dists/android/res/values-nb-rNO/strings.xml
@@ -1,10 +1,25 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Spillmotor for grafikkbaserte eventyr</string>
   <string name="ok">OK</string>
   <string name="quit">Avslutt</string>
+  <string name="no_config_file_title">Feil i konfigurasjonsfil</string>
+  <string name="no_config_file">Lesning eller opprettelse av ScummVM konfigurasjonsfil feilet!</string>
+  <string name="no_save_path_title">Feil i lagringsfilbane</string>
+  <string name="no_save_path_configured">Ã…pning eller opprettelse av standard lagringsfilbane feilet!</string>
+  <string name="no_icons_path_title">Feil i filbane til ikoner</string>
+  <string name="no_icons_path_configured">Ã…pning eller opprettelse av standard ikon- og shaders-filbane feilet!</string>
+  <string name="bad_explicit_save_path_configured">Åpning eller opprettelse av den globalt definerte lagringsfilbanen feilet. Vennligst gå tilbake til standard i ScummVM instillinger</string>
+  <string name="keyboard_toggle_btn_desc">Aktiver virtuelt tastatur</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Avbryt</string>
   <string name="customkeyboardview_keycode_delete">Slett</string>
+  <string name="customkeyboardview_keycode_done">Klar</string>
+  <string name="customkeyboardview_keycode_mode_change">Modusvalg</string>
   <string name="customkeyboardview_keycode_shift">Shift</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Lukk popup</string>
+  <string name="saf_request_prompt">Vennligst velg rotnivå for ditt eksterne (fysiske) SD-kort. Dette er nødvendig for at ScummVM får tillgang til denne filbanen: </string>
+  <string name="saf_revoke_done">Storage Access Framework Permissions for ScummVM ble opphevet!</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-nl-rNL/strings.xml b/dists/android/res/values-nl-rNL/strings.xml
index 481b8279b0f..3fdcb87bf1f 100644
--- a/dists/android/res/values-nl-rNL/strings.xml
+++ b/dists/android/res/values-nl-rNL/strings.xml
@@ -1,10 +1,25 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Grafische avonturen spel engine</string>
   <string name="ok">OK</string>
   <string name="quit">Stoppen</string>
+  <string name="no_config_file_title">Configuratiebestand fout</string>
+  <string name="no_config_file">Kan het ScummVM configuratiebestand niet lezen of creëer een nieuwe!</string>
+  <string name="no_save_path_title">Bewaar Pad Fout</string>
+  <string name="no_save_path_configured">Kon geen toegang krijgen tot het standaard bewaar pad!</string>
+  <string name="no_icons_path_title">Icoon Pad Fout</string>
+  <string name="no_icons_path_configured">Kon geen toegang krijgen tot of aanmaken van het iconen en shaders pad!</string>
+  <string name="bad_explicit_save_path_configured">Kon geen toegang krijgen tot het algemene bewaar pad! Zet deze a.u.b. terug naar standaard in de ScummVM Opties</string>
+  <string name="keyboard_toggle_btn_desc">Virtueel toetsenbord aan-/uitzetten</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Annuleren</string>
   <string name="customkeyboardview_keycode_delete">Verwijderen</string>
+  <string name="customkeyboardview_keycode_done">Klaar</string>
+  <string name="customkeyboardview_keycode_mode_change">Modus verandering</string>
   <string name="customkeyboardview_keycode_shift">Shift</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Popup sluiten</string>
+  <string name="saf_request_prompt">Selecteer a.u.b. de \"root\" map van uw externe (fysieke) SD card. Dit heeft ScummVM nodig om toegang te krijgen tot het pad: </string>
+  <string name="saf_revoke_done">Storage Access Framework rechten van ScummVM zijn teruggetrokken!</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-nn-rNO/strings.xml b/dists/android/res/values-nn-rNO/strings.xml
index 9edfd908729..e79c4952c4d 100644
--- a/dists/android/res/values-nn-rNO/strings.xml
+++ b/dists/android/res/values-nn-rNO/strings.xml
@@ -1,9 +1,14 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">Avslutt ScummVM</string>
   <string name="ok">OK</string>
   <string name="quit">Avslutt</string>
+  <string name="no_save_path_title">Lagringssti:</string>
+  <string name="no_icons_path_title">Ekstrasti:</string>
+  <string name="keyboard_toggle_btn_desc">Virtuelt tastatur</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Avbryt</string>
   <string name="customkeyboardview_keycode_delete">Slett</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Steng</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-pl-rPL/strings.xml b/dists/android/res/values-pl-rPL/strings.xml
index 6cc1c67d67d..303f9ce228c 100644
--- a/dists/android/res/values-pl-rPL/strings.xml
+++ b/dists/android/res/values-pl-rPL/strings.xml
@@ -1,9 +1,25 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Silnik graficzny gier przygodowych</string>
   <string name="ok">OK</string>
   <string name="quit">Zakończ</string>
+  <string name="no_config_file_title">Błąd pliku konfiguracyjnego</string>
+  <string name="no_config_file">Nie można odczytać lub utworzyć pliku konfiguracyjnego ScummVM!</string>
+  <string name="no_save_path_title">Błąd ścieżki zapisów</string>
+  <string name="no_save_path_configured">Nie można utworzyć domyślnej ścieżki zapisu ani uzyskać do niej dostępu!</string>
+  <string name="no_icons_path_title">Błąd ścieżki ikon</string>
+  <string name="no_icons_path_configured">Nie można utworzyć domyślnych ścieżek ikon ani shaderów oraz uzyskać do nich dostępu!</string>
+  <string name="bad_explicit_save_path_configured">Nie można uzyskać dostępu do globalnie ustawionej ścieżki zapisu! Przywróć ustawienia domyślne z opcji ScummVM</string>
+  <string name="keyboard_toggle_btn_desc">Przełącz klawiaturę wirtualną</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Anuluj</string>
-  <string name="customkeyboardview_keycode_delete">Skasuj</string>
+  <string name="customkeyboardview_keycode_delete">Usuń</string>
+  <string name="customkeyboardview_keycode_done">Gotowe</string>
+  <string name="customkeyboardview_keycode_mode_change">Zmiana trybu</string>
+  <string name="customkeyboardview_keycode_shift">Shift</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Zamknij okno</string>
+  <string name="saf_request_prompt">Wybierz katalog główny *root* swojej fizycznej karty SD. Jest to wymagane, aby ScummVM mógł uzyskać dostęp do tej ścieżki: </string>
+  <string name="saf_revoke_done">Uprawnienia dostępu do pamięci masowej dla ScummVM zostały unieważnione!</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-pt-rBR/strings.xml b/dists/android/res/values-pt-rBR/strings.xml
index 5869389bf07..22ebfddb908 100644
--- a/dists/android/res/values-pt-rBR/strings.xml
+++ b/dists/android/res/values-pt-rBR/strings.xml
@@ -1,10 +1,25 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Engine de jogos de aventura</string>
   <string name="ok">OK</string>
   <string name="quit">Sair</string>
+  <string name="no_config_file_title">Erro no Arquivo de Configuração</string>
+  <string name="no_config_file">Não foi possível ler o arquivo de configuração do ScummVM ou criar um novo!</string>
+  <string name="no_save_path_title">Erro na Pasta de Jogo Salvo</string>
+  <string name="no_save_path_configured">Não foi possível criar ou acessar o caminho de salvamento padrão!</string>
+  <string name="no_icons_path_title">Erro na Pasta de Ícones</string>
+  <string name="no_icons_path_configured">Não foi possível criar ou acessar o caminho padrão de ícones e shaders!</string>
+  <string name="bad_explicit_save_path_configured">Não foi possível acessar o caminho de salvamento definido globalmente! Por favor, reverta para o padrão nas Opções do ScummVM</string>
+  <string name="keyboard_toggle_btn_desc">Alternar teclado virtual</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Cancelar</string>
   <string name="customkeyboardview_keycode_delete">Excluir</string>
+  <string name="customkeyboardview_keycode_done">Concluir</string>
+  <string name="customkeyboardview_keycode_mode_change">Alternar modo</string>
   <string name="customkeyboardview_keycode_shift">Shift</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Fechar caixa de diálogo</string>
+  <string name="saf_request_prompt">Selecione a *raiz* do seu cartão SD externo (físico). Isso é necessário para o ScummVM acessar este caminho: </string>
+  <string name="saf_revoke_done">As permissões do Framework de Acesso ao Armazenamento para ScummVM foram revogadas!</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-pt-rPT/strings.xml b/dists/android/res/values-pt-rPT/strings.xml
index 3737419d7bc..841a3553376 100644
--- a/dists/android/res/values-pt-rPT/strings.xml
+++ b/dists/android/res/values-pt-rPT/strings.xml
@@ -1,8 +1,13 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">Sair do ScummVM</string>
   <string name="ok">OK</string>
   <string name="quit">Sair</string>
+  <string name="no_save_path_title">Jogos Guardados:</string>
+  <string name="no_icons_path_title">Ícones:</string>
+  <string name="keyboard_toggle_btn_desc">Teclado virtual</string>
   <string name="customkeyboardview_keycode_cancel">Cancelar</string>
   <string name="customkeyboardview_keycode_delete">Eliminar</string>
   <string name="customkeyboardview_keycode_enter">Centrado</string>
+  <string name="customkeyboardview_popup_close">Fechar</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-ru-rRU/strings.xml b/dists/android/res/values-ru-rRU/strings.xml
index 996adc99aa0..bf20a239783 100644
--- a/dists/android/res/values-ru-rRU/strings.xml
+++ b/dists/android/res/values-ru-rRU/strings.xml
@@ -1,10 +1,25 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Движок графической адвенчуры</string>
   <string name="ok">OK</string>
   <string name="quit">Выход</string>
+  <string name="no_config_file_title">Ошибка файла конфигурации</string>
+  <string name="no_config_file">Невозможно прочитать файл конфигурации ScummVM или создать новый!</string>
+  <string name="no_save_path_title">Ошибка пути сохранения</string>
+  <string name="no_save_path_configured">Невозможно создать или получить доступ к пути сохранения по умолчанию!</string>
+  <string name="no_icons_path_title">Ошибка пути к иконкам</string>
+  <string name="no_icons_path_configured">Невозможно создать или получить доступ к пути иконок и шейдеров!</string>
+  <string name="bad_explicit_save_path_configured">Невозможно получить доступ к глобально установленному пути сохранения! Пожалуйста, вернитесь к параметрам по умолчанию в настройках ScummVM</string>
+  <string name="keyboard_toggle_btn_desc">Переключить виртуальную клавиатуру</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Отмена</string>
   <string name="customkeyboardview_keycode_delete">Удалить</string>
+  <string name="customkeyboardview_keycode_done">Готово</string>
+  <string name="customkeyboardview_keycode_mode_change">Сменить режим</string>
   <string name="customkeyboardview_keycode_shift">Shift</string>
   <string name="customkeyboardview_keycode_enter">Ввод</string>
+  <string name="customkeyboardview_popup_close">Закрыть выпадающий список</string>
+  <string name="saf_request_prompt">Пожалуйста, выберите *корень* вашей внешней (физической) SD-карты. Это необходимо, чтобы ScummVM получил доступ к этой папке: </string>
+  <string name="saf_revoke_done">Разрешения Storage Access Framework для ScummVM были отозваны!</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-sv-rSE/strings.xml b/dists/android/res/values-sv-rSE/strings.xml
index ed0117cd4ed..22c5ff0c762 100644
--- a/dists/android/res/values-sv-rSE/strings.xml
+++ b/dists/android/res/values-sv-rSE/strings.xml
@@ -1,9 +1,14 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">Avsluta ScummVM</string>
   <string name="ok">OK</string>
   <string name="quit">Avsluta</string>
+  <string name="no_save_path_title">Sparsökväg:</string>
+  <string name="no_icons_path_title">Ikon sökväg:</string>
+  <string name="keyboard_toggle_btn_desc">Virtuellt tangentbord</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Avbryt</string>
   <string name="customkeyboardview_keycode_delete">Radera</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Stäng</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-b+tr/strings.xml b/dists/android/res/values-tr/strings.xml
similarity index 56%
rename from dists/android/res/values-b+tr/strings.xml
rename to dists/android/res/values-tr/strings.xml
index 29869de17b2..97f5c48317c 100644
--- a/dists/android/res/values-b+tr/strings.xml
+++ b/dists/android/res/values-tr/strings.xml
@@ -1,8 +1,12 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM\'den çık</string>
   <string name="ok">OK</string>
   <string name="quit">Oyun</string>
+  <string name="no_save_path_title">Kaydetme Yolu:</string>
+  <string name="no_icons_path_title">/kök/ Yolu:</string>
   <string name="customkeyboardview_keycode_cancel">İptal</string>
   <string name="customkeyboardview_keycode_delete">Sil</string>
   <string name="customkeyboardview_keycode_enter">EtkileÅŸim</string>
+  <string name="customkeyboardview_popup_close">Kapat</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-uk-rUA/strings.xml b/dists/android/res/values-uk-rUA/strings.xml
index fa7865067f4..83cff1d4872 100644
--- a/dists/android/res/values-uk-rUA/strings.xml
+++ b/dists/android/res/values-uk-rUA/strings.xml
@@ -1,10 +1,25 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
+  <string name="app_name">ScummVM</string>
+  <string name="app_desc">Движок графічних квестів</string>
   <string name="ok">OK</string>
   <string name="quit">Вихід</string>
+  <string name="no_config_file_title">Помилка файла конфігурації</string>
+  <string name="no_config_file">Не вдалося прочитати файл конфігурації ScummVM або створити новий!</string>
+  <string name="no_save_path_title">Помилка шляху зберігання</string>
+  <string name="no_save_path_configured">Не вдалося створити або дістатися до шляху збереження ігор!</string>
+  <string name="no_icons_path_title">Помилка шляху іконок</string>
+  <string name="no_icons_path_configured">Не вдалося створити або дістатися до шляху іконок та шейдерів!</string>
+  <string name="bad_explicit_save_path_configured">Не вдалося дістатися до глобального шляху збереження ігор! Будь-ласка, поверніться до значень за замовченням в Налаштуваннях ScummVM</string>
+  <string name="keyboard_toggle_btn_desc">Перемикнути віртуальну клавіатуру</string>
   <string name="customkeyboardview_keycode_alt">Alt</string>
   <string name="customkeyboardview_keycode_cancel">Відміна</string>
   <string name="customkeyboardview_keycode_delete">Видалити</string>
+  <string name="customkeyboardview_keycode_done">Готове</string>
+  <string name="customkeyboardview_keycode_mode_change">Змінити режим</string>
   <string name="customkeyboardview_keycode_shift">Shift</string>
   <string name="customkeyboardview_keycode_enter">Enter</string>
+  <string name="customkeyboardview_popup_close">Закрити вискакуючий список</string>
+  <string name="saf_request_prompt">Будь-ласка, оберіть папку *root* (корень) у вашій зовнішній (фізичній) SD-картці. Це необхідно, щоб ScummVM міг дістатися цієї папки: </string>
+  <string name="saf_revoke_done">Дозвіл доступу ScummVM до Storage Access Framework було вилучено!</string>
 </resources>
\ No newline at end of file
diff --git a/dists/android/res/values-zh/strings.xml b/dists/android/res/values-zh/strings.xml
new file mode 100644
index 00000000000..9180679d818
--- /dev/null
+++ b/dists/android/res/values-zh/strings.xml
@@ -0,0 +1,10 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+  <string name="app_name">cummVM</string>
+  <string name="ok">确认</string>
+  <string name="quit">離開</string>
+  <string name="no_save_path_title">保存路径错误</string>
+  <string name="no_icons_path_title">图标路径错误</string>
+  <string name="customkeyboardview_keycode_cancel">取消</string>
+  <string name="customkeyboardview_popup_close">关闭弹出窗口</string>
+</resources>
\ No newline at end of file


Commit: 0c748c894f12b6daee4973c3021f432a20ba2c28
    https://github.com/scummvm/scummvm/commit/0c748c894f12b6daee4973c3021f432a20ba2c28
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Use a dedicated properties file for srcdir

This allows to have a real file for gradle.properties.

Changed paths:
  A dists/android/gradle.properties
    backends/platform/android/android.mk
    dists/android/build.gradle


diff --git a/backends/platform/android/android.mk b/backends/platform/android/android.mk
index 2274098240e..e62fcf88b61 100644
--- a/backends/platform/android/android.mk
+++ b/backends/platform/android/android.mk
@@ -22,10 +22,8 @@ $(PATH_BUILD_GRADLE): $(GRADLE_FILES) | $(PATH_BUILD)
 	$(CP) -r $(PATH_DIST)/gradle/ $(PATH_BUILD)/gradle/
 	$(INSTALL) -c -m 755 $(PATH_DIST)/gradlew $(PATH_BUILD)
 	$(INSTALL) -c -m 644 $(PATH_DIST)/build.gradle $(PATH_BUILD)
-	$(ECHO) "srcdir=$(realpath $(srcdir))\n" > $(PATH_BUILD)/gradle.properties
-	$(ECHO) "org.gradle.jvmargs=-Xmx4096m\n" >> $(PATH_BUILD)/gradle.properties
-	$(ECHO) "android.useAndroidX=true\n" >> $(PATH_BUILD)/gradle.properties
-	$(ECHO) "android.enableJetifier=true\n" >> $(PATH_BUILD)/gradle.properties
+	$(INSTALL) -c -m 644 $(PATH_DIST)/gradle.properties $(PATH_BUILD)
+	$(ECHO) "srcdir=$(realpath $(srcdir))\n" > $(PATH_BUILD)/src.properties
 	$(ECHO) "sdk.dir=$(realpath $(ANDROID_SDK_ROOT))\n" > $(PATH_BUILD)/local.properties
 
 $(PATH_BUILD_ASSETS): $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_HELP) | $(PATH_BUILD)
diff --git a/dists/android/build.gradle b/dists/android/build.gradle
index ae15f6fb31a..eb96ca1119a 100644
--- a/dists/android/build.gradle
+++ b/dists/android/build.gradle
@@ -16,6 +16,10 @@ dependencies {
     }
 }
 
+// Load our source dependent properties
+def srcProperties = new Properties()
+srcProperties.load(new FileInputStream(rootProject.file("src.properties")))
+
 // Enable to see use of deprecated API
 tasks.withType(JavaCompile) {
     options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked"
@@ -102,16 +106,15 @@ android {
             // the Android Gradle plugin. To learn more, go to the section about
             // R8 configuration files.
             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
-            zipAlignEnabled true
         }
     }
     sourceSets {
         main {
             assets.srcDirs 'assets/'
-            java.srcDirs srcdir + '/backends/platform/android/'
+            java.srcDirs srcProperties['srcdir'] + '/backends/platform/android/'
             jniLibs.srcDirs 'lib/'
-            res.srcDirs srcdir + '/dists/android/res/'
-            manifest.srcFile srcdir + '/dists/android/AndroidManifest.xml'
+            res.srcDirs srcProperties['srcdir'] + '/dists/android/res/'
+            manifest.srcFile srcProperties['srcdir'] + '/dists/android/AndroidManifest.xml'
         }
     }
     lintOptions {
diff --git a/dists/android/gradle.properties b/dists/android/gradle.properties
new file mode 100644
index 00000000000..d309f50f163
--- /dev/null
+++ b/dists/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx4096m
+android.useAndroidX=true
+android.enableJetifier=true


Commit: 030c5afc09aef854dce2ce1c457ee486de6d9244
    https://github.com/scummvm/scummvm/commit/030c5afc09aef854dce2ce1c457ee486de6d9244
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Upgrade build tools and fix deprecation warnings

This makes the project build in Android Studio without any warning
except the Android version as API level 34 is not yet targeted.

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 eb96ca1119a..be202cc20f4 100644
--- a/dists/android/build.gradle
+++ b/dists/android/build.gradle
@@ -5,7 +5,7 @@ buildscript {
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:7.4.2'
+        classpath 'com.android.tools.build:gradle:8.4.0'
     }
 }
 
@@ -21,8 +21,8 @@ def srcProperties = new Properties()
 srcProperties.load(new FileInputStream(rootProject.file("src.properties")))
 
 // Enable to see use of deprecated API
-tasks.withType(JavaCompile) {
-    options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked"
+tasks.withType(JavaCompile).configureEach {
+	options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked"
 }
 
 //gradle.startParameter.showStacktrace = ShowStacktrace.ALWAYS_FULL
@@ -31,7 +31,6 @@ apply plugin: 'com.android.application'
 
 android {
     compileSdk 33
-    buildToolsVersion "33.0.1"
     ndkVersion "21.3.6528147"
 
     namespace "org.scummvm.scummvm"
@@ -39,7 +38,7 @@ android {
     defaultConfig {
         applicationId "org.scummvm.scummvm"
 
-        setProperty("archivesBaseName", "ScummVM")
+        base.archivesName = "ScummVM"
 
         minSdkVersion 16
         targetSdkVersion 33
@@ -87,7 +86,7 @@ android {
     }
     buildTypes {
         debug{
-            applicationIdSuffix "debug"
+            applicationIdSuffix ".debug"
             manifestPlaceholders = [nameSuffix:"_debug"]
             debuggable true
         }
@@ -123,6 +122,6 @@ android {
 }
 
 dependencies {
-    implementation "androidx.annotation:annotation:1.5.0"
+    implementation "androidx.annotation:annotation:1.7.1"
     implementation "androidx.appcompat:appcompat:1.6.1"
 }
diff --git a/dists/android/gradle/wrapper/gradle-wrapper.jar b/dists/android/gradle/wrapper/gradle-wrapper.jar
index 943f0cbfa75..d64cd491770 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 f398c33c4b0..a80b22ce5cf 100644
--- a/dists/android/gradle/wrapper/gradle-wrapper.properties
+++ b/dists/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
 networkTimeout=10000
+validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/dists/android/gradlew b/dists/android/gradlew
index cccdd3d517f..1aa94a42690 100755
--- a/dists/android/gradlew
+++ b/dists/android/gradlew
@@ -1,78 +1,127 @@
-#!/usr/bin/env sh
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
 
 ##############################################################################
-##
-##  Gradle start up script for UN*X
-##
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       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
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
 ##############################################################################
 
 # Attempt to set APP_HOME
+
 # Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
 done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
 
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# This is normally unused
+# 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
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
 
 warn () {
     echo "$*"
-}
+} >&2
 
 die () {
     echo
     echo "$*"
     echo
     exit 1
-}
+} >&2
 
 # OS specific support (must be 'true' or 'false').
 cygwin=false
 msys=false
 darwin=false
 nonstop=false
-case "`uname`" in
-  CYGWIN* )
-    cygwin=true
-    ;;
-  Darwin* )
-    darwin=true
-    ;;
-  MINGW* )
-    msys=true
-    ;;
-  NONSTOP* )
-    nonstop=true
-    ;;
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
 esac
 
 CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
 
+
 # Determine the Java command to use to start the JVM.
 if [ -n "$JAVA_HOME" ] ; then
     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
         # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD="$JAVA_HOME/jre/sh/java"
+        JAVACMD=$JAVA_HOME/jre/sh/java
     else
-        JAVACMD="$JAVA_HOME/bin/java"
+        JAVACMD=$JAVA_HOME/bin/java
     fi
     if [ ! -x "$JAVACMD" ] ; then
         die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -81,92 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
     fi
 else
-    JAVACMD="java"
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
+    fi
 fi
 
 # Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
-    MAX_FD_LIMIT=`ulimit -H -n`
-    if [ $? -eq 0 ] ; then
-        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
-            MAX_FD="$MAX_FD_LIMIT"
-        fi
-        ulimit -n $MAX_FD
-        if [ $? -ne 0 ] ; then
-            warn "Could not set maximum file descriptor limit: $MAX_FD"
-        fi
-    else
-        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
-    fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
 fi
 
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
-    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
 
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
-    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
-    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-    JAVACMD=`cygpath --unix "$JAVACMD"`
-
-    # We build the pattern for arguments to be converted via cygpath
-    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
-    SEP=""
-    for dir in $ROOTDIRSRAW ; do
-        ROOTDIRS="$ROOTDIRS$SEP$dir"
-        SEP="|"
-    done
-    OURCYGPATTERN="(^($ROOTDIRS))"
-    # Add a user-defined pattern to the cygpath arguments
-    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
-        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
-    fi
     # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    i=0
-    for arg in "$@" ; do
-        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
-        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
-
-        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
-            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
-        else
-            eval `echo args$i`="\"$arg\""
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
         fi
-        i=$((i+1))
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
     done
-    case $i in
-        (0) set -- ;;
-        (1) set -- "$args0" ;;
-        (2) set -- "$args0" "$args1" ;;
-        (3) set -- "$args0" "$args1" "$args2" ;;
-        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
-    esac
 fi
 
-# Escape application args
-save () {
-    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
-    echo " "
-}
-APP_ARGS=$(save "$@")
-
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
 
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
-  cd "$(dirname "$0")"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
 fi
 
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
 exec "$JAVACMD" "$@"


Commit: 30c8b3eec3e0b741db194ccccff505e79ba7fe8f
    https://github.com/scummvm/scummvm/commit/30c8b3eec3e0b741db194ccccff505e79ba7fe8f
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Use a dedicated INI parser for our configuration file

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


diff --git a/backends/platform/android/org/scummvm/scummvm/INIParser.java b/backends/platform/android/org/scummvm/scummvm/INIParser.java
new file mode 100644
index 00000000000..381dddca3a9
--- /dev/null
+++ b/backends/platform/android/org/scummvm/scummvm/INIParser.java
@@ -0,0 +1,140 @@
+package org.scummvm.scummvm;
+
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * INI file parser modeled after config manager one in C++ side
+ */
+public class INIParser {
+	private final static String LOG_TAG = "INIParser";
+
+	// This class can not be instantiated
+	private INIParser() {
+	}
+
+	public static Map<String, Map<String, String>> parse(Reader reader) throws IOException {
+		Map<String, Map<String, String>> ret = new HashMap<>();
+		BufferedReader lineReader = new BufferedReader(reader);
+		Map<String, String> domain = null;
+		int lineno = 0;
+		String line;
+
+		while ((line = lineReader.readLine()) != null) {
+			lineno++;
+
+			if (lineno == 1 && line.startsWith("\357\273\277")) {
+				line = line.substring(3);
+			}
+
+			if (line.isEmpty()) {
+				continue;
+			}
+
+			final char firstChar = line.charAt(0);
+
+			/* Unlike C++ parser, we ignore comments for simplicity */
+			if (firstChar == '#') {
+				continue;
+			}
+
+			if (firstChar == '[') {
+				int i;
+				for(i = 1; i < line.length(); i++) {
+					final char c = line.charAt(i);
+					if (c > 127) {
+						break;
+					}
+					if (!Character.isLetterOrDigit(c) && c != '-' && c != '_') {
+						break;
+					}
+				}
+
+				if (i == line.length()) {
+					return null;
+				}
+				if (line.charAt(i) != ']') {
+					return null;
+				}
+
+				String domainName = line.substring(1, i);
+				domain = new HashMap<>();
+				ret.put(domainName, domain);
+
+				continue;
+			}
+
+			int i;
+			for (i = 0; i < line.length(); i++) {
+				final char c = line.charAt(i);
+				if (!isSpace(c)) {
+					break;
+				}
+			}
+
+			if (i == line.length()) {
+				continue;
+			}
+
+			if (domain == null) {
+				return null;
+			}
+
+			int equal = line.indexOf('=');
+			if (equal == -1) {
+				return null;
+			}
+
+			String key = line.substring(i, equal);
+			String value = line.substring(equal + 1);
+
+			key = trim(key);
+			value = trim(value);
+
+			domain.put(key, value);
+		}
+
+		return ret;
+	}
+
+	public static String get(Map<String, Map<String, String>> ini, String section, String key, String defaultValue) {
+		if (ini == null) {
+			return defaultValue;
+		}
+		Map<String, String> s = ini.get(section);
+		if (s == null) {
+			return defaultValue;
+		}
+		String value = s.get(key);
+		if (value == null) {
+			return defaultValue;
+		}
+		return value;
+	}
+
+	/* Java isWhitespace is more inclusive than C one */
+	private static boolean isSpace(char c) {
+		return (c == ' ' || c == '\f' || c == '\n' || c == '\r' || c == '\t' || c == '\013');
+	}
+
+	/* Java trim is more strict than C one */
+	private static String trim(String s) {
+		int begin, end;
+		for(begin = 0; begin < s.length(); begin++) {
+			if (!isSpace(s.charAt(begin))) {
+				break;
+			}
+		}
+		for(end = s.length() - 1; end > begin; end--) {
+			if (!isSpace(s.charAt(end))) {
+				break;
+			}
+		}
+		return s.substring(begin, end + 1);
+	}
+}
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
index 665ae72ad87..16845bae473 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -49,7 +49,6 @@ import android.widget.Toast;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
-import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -57,9 +56,7 @@ import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.Reader;
 import java.io.UnsupportedEncodingException;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -1357,58 +1354,14 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 		}
 	}
 
-	/**
-	 * Auxiliary function to read our ini configuration file
-	 * Code is from https://stackoverflow.com/a/41084504
-	 * returns The sections of the ini file as a Map of the header Strings to a Properties object (the key=value list of each section)
-	 */
-	@TargetApi(Build.VERSION_CODES.GINGERBREAD)
-	private static Map<String, Properties> parseINI(Reader reader) throws IOException {
-		final HashMap<String, Properties> result = new HashMap<>();
-		new Properties() {
-
-			private Properties section;
-
-			@Override
-			public Object put(Object key, Object value) {
-				String header = (key + " " + value).trim();
-				if (header.startsWith("[") && header.endsWith("]"))
-					return result.put(header.substring(1, header.length() - 1),
-						section = new Properties());
-				else
-					return section.put(key, value);
-			}
-
-		}.load(reader);
-		return result;
-	}
-
 	private static String getVersionInfoFromScummvmConfiguration(String fullIniFilePath) {
-		try (BufferedReader bufferedReader = new BufferedReader(new FileReader(fullIniFilePath))) {
-			Map<String, Properties> parsedIniMap = parseINI(bufferedReader);
-			if (!parsedIniMap.isEmpty()
-			    && parsedIniMap.containsKey("scummvm")
-			    && parsedIniMap.get("scummvm") != null) {
-				return parsedIniMap.get("scummvm").getProperty("versioninfo", "");
-			}
+		Map<String, Map<String, String>> parsedIniMap;
+		try (FileReader reader = new FileReader(fullIniFilePath)) {
+			parsedIniMap = INIParser.parse(reader);
 		} catch (IOException ignored) {
-		} catch (NullPointerException ignored) {
-		}
-		return "";
-	}
-
-	private static String getSavepathInfoFromScummvmConfiguration(String fullIniFilePath) {
-		try (BufferedReader bufferedReader = new BufferedReader(new FileReader(fullIniFilePath))) {
-			Map<String, Properties> parsedIniMap = parseINI(bufferedReader);
-			if (!parsedIniMap.isEmpty()
-			    && parsedIniMap.containsKey("scummvm")
-			    && parsedIniMap.get("scummvm") != null) {
-				return parsedIniMap.get("scummvm").getProperty("savepath", "");
-			}
-		} catch (IOException ignored) {
-		} catch (NullPointerException ignored) {
+			return null;
 		}
-		return "";
+		return INIParser.get(parsedIniMap, "scummvm", "versioninfo", null);
 	}
 
 	private boolean seekAndInitScummvmConfiguration() {
@@ -1570,9 +1523,9 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 				Log.d(ScummVM.LOG_TAG, "ScummVM Config file already exists!");
 				Log.d(ScummVM.LOG_TAG, "Existing ScummVM INI: " + _configScummvmFile.getPath());
 				String existingVersionInfo = getVersionInfoFromScummvmConfiguration(_configScummvmFile.getPath());
-				if (!TextUtils.isEmpty(existingVersionInfo) && !TextUtils.isEmpty(existingVersionInfo.trim()) ) {
-					Log.d(ScummVM.LOG_TAG, "Existing ScummVM Version: " + existingVersionInfo.trim());
-					Version tmpOldVersionFound = new Version(existingVersionInfo.trim());
+				if (!TextUtils.isEmpty(existingVersionInfo) && !TextUtils.isEmpty(existingVersionInfo) ) {
+					Log.d(ScummVM.LOG_TAG, "Existing ScummVM Version: " + existingVersionInfo);
+					Version tmpOldVersionFound = new Version(existingVersionInfo);
 					if (tmpOldVersionFound.compareTo(maxOldVersionFound) > 0) {
 						maxOldVersionFound = tmpOldVersionFound;
 						existingVersionFoundInScummVMDataDir = tmpOldVersionFound;


Commit: b3e5cf8e58880deb936f38b8661b812211192677
    https://github.com/scummvm/scummvm/commit/b3e5cf8e58880deb936f38b8661b812211192677
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Optimize imports

Changed paths:
    backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
    backends/platform/android/org/scummvm/scummvm/CustomKeyboardView.java
    backends/platform/android/org/scummvm/scummvm/EditableSurfaceView.java
    backends/platform/android/org/scummvm/scummvm/ExternalStorage.java
    backends/platform/android/org/scummvm/scummvm/MouseHelper.java
    backends/platform/android/org/scummvm/scummvm/MultitouchHelper.java
    backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
    backends/platform/android/org/scummvm/scummvm/ScummVM.java
    backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
    backends/platform/android/org/scummvm/scummvm/ScummVMEventsBase.java
    backends/platform/android/org/scummvm/scummvm/ScummVMEventsModern.java
    backends/platform/android/org/scummvm/scummvm/SplashActivity.java


diff --git a/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java b/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
index 8561abd7868..7f1b8df1910 100644
--- a/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
+++ b/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
@@ -4,7 +4,6 @@ import android.media.AudioAttributes;
 import android.media.AudioFormat;
 import android.media.AudioManager;
 import android.media.AudioTrack;
-
 import android.view.View;
 import android.view.Window;
 import android.view.WindowInsets;
diff --git a/backends/platform/android/org/scummvm/scummvm/CustomKeyboardView.java b/backends/platform/android/org/scummvm/scummvm/CustomKeyboardView.java
index 1bb04425292..4608804e7cd 100755
--- a/backends/platform/android/org/scummvm/scummvm/CustomKeyboardView.java
+++ b/backends/platform/android/org/scummvm/scummvm/CustomKeyboardView.java
@@ -30,12 +30,10 @@ import android.graphics.Rect;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.media.AudioManager;
-//import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.util.TypedValue;
 import android.view.GestureDetector;
 import android.view.Gravity;
diff --git a/backends/platform/android/org/scummvm/scummvm/EditableSurfaceView.java b/backends/platform/android/org/scummvm/scummvm/EditableSurfaceView.java
index 631f411bdb6..ff80eb7c1e4 100644
--- a/backends/platform/android/org/scummvm/scummvm/EditableSurfaceView.java
+++ b/backends/platform/android/org/scummvm/scummvm/EditableSurfaceView.java
@@ -1,5 +1,6 @@
 package org.scummvm.scummvm;
 
+import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Build;
@@ -7,7 +8,6 @@ import android.text.Editable;
 import android.text.InputType;
 import android.text.Selection;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.PointerIcon;
@@ -15,7 +15,6 @@ import android.view.SurfaceView;
 import android.view.inputmethod.BaseInputConnection;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
-import android.annotation.TargetApi;
 
 public class EditableSurfaceView extends SurfaceView {
 	final Context _context;
diff --git a/backends/platform/android/org/scummvm/scummvm/ExternalStorage.java b/backends/platform/android/org/scummvm/scummvm/ExternalStorage.java
index 78a159254e6..d0aa27fbc92 100644
--- a/backends/platform/android/org/scummvm/scummvm/ExternalStorage.java
+++ b/backends/platform/android/org/scummvm/scummvm/ExternalStorage.java
@@ -1,23 +1,23 @@
 package org.scummvm.scummvm;
 
 import android.content.Context;
+import android.os.Build;
 import android.os.Environment;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Scanner;
-
 import android.text.TextUtils;
+import android.util.Log;
+
 import java.io.BufferedReader;
+import java.io.File;
 import java.io.FileReader;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Locale;
+import java.util.Scanner;
 import java.util.regex.Pattern;
-import android.util.Log;
-import android.os.Build;
 
 
 /**
diff --git a/backends/platform/android/org/scummvm/scummvm/MouseHelper.java b/backends/platform/android/org/scummvm/scummvm/MouseHelper.java
index 9c241a2ae50..a8d2b1f6e8e 100644
--- a/backends/platform/android/org/scummvm/scummvm/MouseHelper.java
+++ b/backends/platform/android/org/scummvm/scummvm/MouseHelper.java
@@ -2,11 +2,9 @@ package org.scummvm.scummvm;
 
 import android.annotation.SuppressLint;
 import android.os.Build;
-//import android.util.Log;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
-//import android.view.SurfaceView;
 import android.view.View;
 
 /**
diff --git a/backends/platform/android/org/scummvm/scummvm/MultitouchHelper.java b/backends/platform/android/org/scummvm/scummvm/MultitouchHelper.java
index 130cd51a3ba..5aa81511063 100644
--- a/backends/platform/android/org/scummvm/scummvm/MultitouchHelper.java
+++ b/backends/platform/android/org/scummvm/scummvm/MultitouchHelper.java
@@ -1,20 +1,19 @@
 
 package org.scummvm.scummvm;
 
+import static org.scummvm.scummvm.ScummVMEventsBase.JE_MOUSE_WHEEL_DOWN;
+import static org.scummvm.scummvm.ScummVMEventsBase.JE_MOUSE_WHEEL_UP;
+import static org.scummvm.scummvm.ScummVMEventsBase.JE_MULTI;
+
+import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.util.Log;
 import android.view.MotionEvent;
-import android.os.Handler;
 
 import androidx.annotation.NonNull;
 
 import java.lang.ref.WeakReference;
 
-import static org.scummvm.scummvm.ScummVMEventsBase.JE_MOUSE_WHEEL_DOWN;
-import static org.scummvm.scummvm.ScummVMEventsBase.JE_MOUSE_WHEEL_UP;
-import static org.scummvm.scummvm.ScummVMEventsBase.JE_MULTI;
-
 public class MultitouchHelper {
 	private final ScummVM _scummvm;
 
@@ -254,7 +253,7 @@ public class MultitouchHelper {
 									//  - and the movement distance of both fingers on y axis is around >= MOVE_THRESHOLD_FOR_TOUCH_MOUSE_WHEEL_DECISION
 									//         (plus some other qualifying checks to determine significant and similar movement on both fingers)
 									// NOTE the movementOfFinger2onY and movementOfFinger1onY gets higher (on subsequent events)
-									//     if the user keeps moving their fingers (in the same direction), 
+									//     if the user keeps moving their fingers (in the same direction),
 									//     since it's in reference to the starting points for the fingers
 									int movementOfFinger2onY = actionEventY - _cachedActionEventOnPointer2DownY;
 									int movementOfFinger1onY = actionEventFirstPointerCoordY - _cachedActionEventOnPointer1DownY;
diff --git a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
index e9f9ffaed8f..7fe93383e70 100644
--- a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
+++ b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
@@ -1,15 +1,5 @@
 package org.scummvm.scummvm;
 
-import java.io.FileNotFoundException;
-
-import java.lang.ref.SoftReference;
-
-import java.util.ArrayDeque;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-
-import android.annotation.SuppressLint;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -23,6 +13,12 @@ import android.util.Log;
 
 import androidx.annotation.RequiresApi;
 
+import java.io.FileNotFoundException;
+import java.lang.ref.SoftReference;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.HashMap;
+
 /**
  * SAF primitives for C++ FSNode
  */
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVM.java b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
index bb0a0f38b71..d9208e298c3 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVM.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
@@ -1,16 +1,16 @@
 package org.scummvm.scummvm;
 
-import androidx.annotation.NonNull;
 import android.content.res.AssetManager;
 import android.graphics.Bitmap;
 import android.graphics.PixelFormat;
 import android.media.AudioFormat;
 import android.media.AudioManager;
 import android.media.AudioTrack;
-import android.os.Build;
 import android.util.Log;
 import android.view.SurfaceHolder;
 
+import androidx.annotation.NonNull;
+
 import java.util.LinkedHashMap;
 import java.util.Locale;
 import java.util.Scanner;
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
index 16845bae473..483b0dd850b 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -1,5 +1,8 @@
 package org.scummvm.scummvm;
 
+import static android.content.res.Configuration.HARDKEYBOARDHIDDEN_NO;
+import static android.content.res.Configuration.KEYBOARD_QWERTY;
+
 import android.Manifest;
 import android.annotation.TargetApi;
 import android.app.Activity;
@@ -8,8 +11,6 @@ import android.content.ClipboardManager;
 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;
@@ -23,7 +24,6 @@ import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
-import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.os.SystemClock;
 import android.provider.DocumentsContract;
@@ -34,7 +34,6 @@ import android.util.TypedValue;
 import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
-import android.view.PointerIcon;
 import android.view.SurfaceHolder;
 import android.view.View;
 import android.view.ViewGroup;
@@ -56,16 +55,11 @@ import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.Properties;
 import java.util.TreeSet;
 
-import static android.content.res.Configuration.HARDKEYBOARDHIDDEN_NO;
-import static android.content.res.Configuration.KEYBOARD_QWERTY;
-
 public class ScummVMActivity extends Activity implements OnKeyboardVisibilityListener {
 
 	/* Establish whether the hover events are available */
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMEventsBase.java b/backends/platform/android/org/scummvm/scummvm/ScummVMEventsBase.java
index 87dea9af551..65f1fce55a3 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMEventsBase.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMEventsBase.java
@@ -1,20 +1,17 @@
 package org.scummvm.scummvm;
 
+import android.content.Context;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.content.Context;
-import android.util.Log;
-import android.view.KeyEvent;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
 import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
-import android.view.GestureDetector;
-import android.view.HapticFeedbackConstants;
-import android.view.InputDevice;
-//import android.view.inputmethod.InputMethodManager;
 
 import androidx.annotation.NonNull;
 
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMEventsModern.java b/backends/platform/android/org/scummvm/scummvm/ScummVMEventsModern.java
index 24d724e559b..0b6187c18ad 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMEventsModern.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMEventsModern.java
@@ -4,17 +4,12 @@ import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-//import android.util.Log;
-import android.view.MotionEvent;
 import android.view.InputDevice;
+import android.view.MotionEvent;
 
 import androidx.annotation.NonNull;
 
 import java.lang.ref.WeakReference;
-//import java.util.ArrayList;
-//import java.util.Collections;
-//import java.util.Comparator;
-//import java.util.List;
 
 // A class that extends the basic ScummVMEventsBase, supporting Android APIs > HONEYCOMB_MR1 (API 12)
 public class ScummVMEventsModern extends ScummVMEventsBase {
diff --git a/backends/platform/android/org/scummvm/scummvm/SplashActivity.java b/backends/platform/android/org/scummvm/scummvm/SplashActivity.java
index c9f7acdd3ae..91d53fd4ecb 100644
--- a/backends/platform/android/org/scummvm/scummvm/SplashActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/SplashActivity.java
@@ -1,16 +1,14 @@
 package org.scummvm.scummvm;
 
 import android.Manifest;
-import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.content.res.Configuration;
 import android.os.Build;
 import android.os.Bundle;
 import android.util.Log;
-import android.view.View;
 import android.widget.Toast;
-import android.content.res.Configuration;
 
 import androidx.annotation.NonNull;
 


Commit: 12cf731fb614a00182d7c78cef8f28e88c550a63
    https://github.com/scummvm/scummvm/commit/12cf731fb614a00182d7c78cef8f28e88c550a63
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Fix style of manifest

Changed paths:
    dists/android/AndroidManifest.xml


diff --git a/dists/android/AndroidManifest.xml b/dists/android/AndroidManifest.xml
index 00139f9fe67..3b98d6ef34c 100644
--- a/dists/android/AndroidManifest.xml
+++ b/dists/android/AndroidManifest.xml
@@ -7,7 +7,8 @@
 	android:sharedUserMaxSdkVersion="32"
 	tools:targetApi="tiramisu">
 
-	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+	<uses-permission
+		android:name="android.permission.WRITE_EXTERNAL_STORAGE"
 		tools:ignore="ScopedStorage" />
 	<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 	<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
@@ -28,26 +29,26 @@
 		android:required="true" />
 
 	<supports-screens
-		android:resizeable="true"
 		android:largeScreens="true"
 		android:normalScreens="true"
+		android:resizeable="true"
 		android:smallScreens="true"
 		android:xlargeScreens="true" />
 
 	<application
 		android:allowBackup="true"
+		android:appCategory="game"
 		android:description="@string/app_desc"
 		android:icon="@mipmap/scummvm"
-		android:appCategory="game"
 		android:label="@string/app_name${nameSuffix}"
 		android:resizeableActivity="false"
 		android:requestLegacyExternalStorage="true">
 		<activity
 			android:name=".SplashActivity"
-			android:exported="true"
 			android:banner="@drawable/leanback_icon"
 			android:configChanges="orientation|keyboard|keyboardHidden|navigation|screenSize|smallestScreenSize|screenLayout"
 			android:resizeableActivity="true"
+			android:exported="true"
 			android:theme="@style/SplashTheme"
 			android:windowSoftInputMode="adjustResize">
 			<intent-filter>
@@ -60,14 +61,13 @@
 		</activity>
 		<activity
 			android:name=".ScummVMActivity"
-			android:exported="false"
 			android:banner="@drawable/leanback_icon"
 			android:configChanges="orientation|keyboard|keyboardHidden|navigation|screenSize|smallestScreenSize|screenLayout"
 			android:resizeableActivity="true"
+			android:exported="false"
 			android:launchMode="singleInstance"
 			android:theme="@style/AppTheme"
-			android:windowSoftInputMode="adjustResize">
-		</activity>
+			android:windowSoftInputMode="adjustResize" />
 	</application>
 
 </manifest>


Commit: 497cd574835cfddab61b52e10f1821825608c016
    https://github.com/scummvm/scummvm/commit/497cd574835cfddab61b52e10f1821825608c016
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Fix deprecation warnings in Manifest

Changed paths:
    dists/android/AndroidManifest.xml


diff --git a/dists/android/AndroidManifest.xml b/dists/android/AndroidManifest.xml
index 3b98d6ef34c..507b6ca8cf4 100644
--- a/dists/android/AndroidManifest.xml
+++ b/dists/android/AndroidManifest.xml
@@ -10,7 +10,9 @@
 	<uses-permission
 		android:name="android.permission.WRITE_EXTERNAL_STORAGE"
 		tools:ignore="ScopedStorage" />
-	<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+	<uses-permission
+		android:name="android.permission.READ_EXTERNAL_STORAGE"
+		android:maxSdkVersion="32" />
 	<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 	<uses-permission android:name="android.permission.INTERNET" />
 	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -41,13 +43,11 @@
 		android:description="@string/app_desc"
 		android:icon="@mipmap/scummvm"
 		android:label="@string/app_name${nameSuffix}"
-		android:resizeableActivity="false"
 		android:requestLegacyExternalStorage="true">
 		<activity
 			android:name=".SplashActivity"
 			android:banner="@drawable/leanback_icon"
 			android:configChanges="orientation|keyboard|keyboardHidden|navigation|screenSize|smallestScreenSize|screenLayout"
-			android:resizeableActivity="true"
 			android:exported="true"
 			android:theme="@style/SplashTheme"
 			android:windowSoftInputMode="adjustResize">
@@ -63,7 +63,6 @@
 			android:name=".ScummVMActivity"
 			android:banner="@drawable/leanback_icon"
 			android:configChanges="orientation|keyboard|keyboardHidden|navigation|screenSize|smallestScreenSize|screenLayout"
-			android:resizeableActivity="true"
 			android:exported="false"
 			android:launchMode="singleInstance"
 			android:theme="@style/AppTheme"


Commit: 59d4855a925c8572db5107c1c7c67449603c3f48
    https://github.com/scummvm/scummvm/commit/59d4855a925c8572db5107c1c7c67449603c3f48
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Disable Jetifier

We only use androidx and no other library which could need it

Changed paths:
    dists/android/gradle.properties


diff --git a/dists/android/gradle.properties b/dists/android/gradle.properties
index d309f50f163..43caf404b82 100644
--- a/dists/android/gradle.properties
+++ b/dists/android/gradle.properties
@@ -1,3 +1,3 @@
 org.gradle.jvmargs=-Xmx4096m
 android.useAndroidX=true
-android.enableJetifier=true
+android.enableJetifier=false


Commit: 074344f84ef8ee82e82e2cea9a1dde4a14775ab6
    https://github.com/scummvm/scummvm/commit/074344f84ef8ee82e82e2cea9a1dde4a14775ab6
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Fix dependencies in port Makefile

Every generated file has now its rule and APKs depend on all the
generated files

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


diff --git a/backends/platform/android/android.mk b/backends/platform/android/android.mk
index e62fcf88b61..257ffe349a6 100644
--- a/backends/platform/android/android.mk
+++ b/backends/platform/android/android.mk
@@ -1,10 +1,10 @@
 # Android specific build targets
 PATH_DIST = $(srcdir)/dists/android
 
-GRADLE_FILES = $(shell find $(PATH_DIST)/gradle -type f) $(PATH_DIST)/gradlew $(PATH_DIST)/build.gradle
+GRADLE_FILES = $(shell find $(PATH_DIST)/gradle -type f)
 
 PATH_BUILD = ./android_project
-PATH_BUILD_GRADLE = $(PATH_BUILD)/build.gradle
+PATH_BUILD_GRADLE = $(PATH_BUILD)/gradle/.timestamp $(PATH_BUILD)/gradlew $(PATH_BUILD)/build.gradle $(PATH_BUILD)/gradle.properties $(PATH_BUILD)/local.properties $(PATH_BUILD)/src.properties
 PATH_BUILD_ASSETS = $(PATH_BUILD)/assets
 PATH_BUILD_LIB = $(PATH_BUILD)/lib/$(ABI)
 PATH_BUILD_LIBSCUMMVM = $(PATH_BUILD)/lib/$(ABI)/libscummvm.so
@@ -18,14 +18,25 @@ DIST_FILES_HELP = $(PATH_DIST)/android-help.zip
 $(PATH_BUILD):
 	$(MKDIR) $(PATH_BUILD)
 
-$(PATH_BUILD_GRADLE): $(GRADLE_FILES) | $(PATH_BUILD)
-	$(CP) -r $(PATH_DIST)/gradle/ $(PATH_BUILD)/gradle/
+$(PATH_BUILD)/gradle/.timestamp: $(GRADLE_FILES)
+	$(CP) -r $(PATH_DIST)/gradle/. $(PATH_BUILD)/gradle/
+	touch "$@"
+
+$(PATH_BUILD)/gradlew: $(PATH_DIST)/gradlew
 	$(INSTALL) -c -m 755 $(PATH_DIST)/gradlew $(PATH_BUILD)
+
+$(PATH_BUILD)/build.gradle: $(PATH_DIST)/build.gradle | $(PATH_BUILD)
 	$(INSTALL) -c -m 644 $(PATH_DIST)/build.gradle $(PATH_BUILD)
+
+$(PATH_BUILD)/gradle.properties: $(PATH_DIST)/gradle.properties | $(PATH_BUILD)
 	$(INSTALL) -c -m 644 $(PATH_DIST)/gradle.properties $(PATH_BUILD)
-	$(ECHO) "srcdir=$(realpath $(srcdir))\n" > $(PATH_BUILD)/src.properties
+
+$(PATH_BUILD)/local.properties: configure.stamp | $(PATH_BUILD)
 	$(ECHO) "sdk.dir=$(realpath $(ANDROID_SDK_ROOT))\n" > $(PATH_BUILD)/local.properties
 
+$(PATH_BUILD)/src.properties: configure.stamp | $(PATH_BUILD)
+	$(ECHO) "srcdir=$(realpath $(srcdir))\n" > $(PATH_BUILD)/src.properties
+
 $(PATH_BUILD_ASSETS): $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_HELP) | $(PATH_BUILD)
 	$(INSTALL) -d $(PATH_BUILD_ASSETS)
 	$(INSTALL) -c -m 644 $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_HELP) $(PATH_BUILD_ASSETS)/


Commit: 4ecf2e4ccc39adedb1dcd45f890d3520b9015342
    https://github.com/scummvm/scummvm/commit/4ecf2e4ccc39adedb1dcd45f890d3520b9015342
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Allow to use an Intent to start a specific game

UI to create such intents will come in a later commit.
If the game is already running, it's resumed where it was left when
ScummVM was paused.

Changed paths:
    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
    backends/platform/android/org/scummvm/scummvm/SplashActivity.java


diff --git a/backends/platform/android/android.cpp b/backends/platform/android/android.cpp
index d8084b11585..8aee751d060 100644
--- a/backends/platform/android/android.cpp
+++ b/backends/platform/android/android.cpp
@@ -575,11 +575,13 @@ void OSystem_Android::engineInit() {
 	_engineRunning = true;
 	updateOnScreenControls();
 
+	JNI::setCurrentGame(ConfMan.getActiveDomainName());
 }
 
 void OSystem_Android::engineDone() {
 	_engineRunning = false;
 	updateOnScreenControls();
+	JNI::setCurrentGame("");
 }
 
 void OSystem_Android::updateOnScreenControls() {
diff --git a/backends/platform/android/jni-android.cpp b/backends/platform/android/jni-android.cpp
index 80c796f48a5..a8f42e98906 100644
--- a/backends/platform/android/jni-android.cpp
+++ b/backends/platform/android/jni-android.cpp
@@ -98,6 +98,7 @@ jmethodID JNI::_MID_setOrientation = 0;
 jmethodID JNI::_MID_getScummVMBasePath;
 jmethodID JNI::_MID_getScummVMConfigPath;
 jmethodID JNI::_MID_getScummVMLogPath;
+jmethodID JNI::_MID_setCurrentGame = 0;
 jmethodID JNI::_MID_getSysArchives = 0;
 jmethodID JNI::_MID_getAllStorageLocations = 0;
 jmethodID JNI::_MID_initSurface = 0;
@@ -614,6 +615,27 @@ Common::String JNI::getScummVMLogPath() {
 	return path;
 }
 
+void JNI::setCurrentGame(const Common::String &target) {
+	JNIEnv *env = JNI::getEnv();
+	jstring java_target = nullptr;
+	if (!target.empty()) {
+		java_target = convertToJString(env, Common::U32String(target));
+	}
+
+	env->CallVoidMethod(_jobj, _MID_setCurrentGame, java_target);
+
+	if (env->ExceptionCheck()) {
+		LOGE("Failed to set current game");
+
+		env->ExceptionDescribe();
+		env->ExceptionClear();
+	}
+
+	if (java_target) {
+		env->DeleteLocalRef(java_target);
+	}
+}
+
 // 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 base directory, with a higher priority
@@ -804,6 +826,7 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
 	FIND_METHOD(, getScummVMBasePath, "()Ljava/lang/String;");
 	FIND_METHOD(, getScummVMConfigPath, "()Ljava/lang/String;");
 	FIND_METHOD(, getScummVMLogPath, "()Ljava/lang/String;");
+	FIND_METHOD(, setCurrentGame, "(Ljava/lang/String;)V");
 	FIND_METHOD(, getSysArchives, "()[Ljava/lang/String;");
 	FIND_METHOD(, getAllStorageLocations, "()[Ljava/lang/String;");
 	FIND_METHOD(, initSurface, "()Ljavax/microedition/khronos/egl/EGLSurface;");
diff --git a/backends/platform/android/jni-android.h b/backends/platform/android/jni-android.h
index 6b887e9bc29..29dae5ebf22 100644
--- a/backends/platform/android/jni-android.h
+++ b/backends/platform/android/jni-android.h
@@ -101,6 +101,7 @@ public:
 	static Common::String getScummVMConfigPath();
 	static Common::String getScummVMLogPath();
 	static jint getAndroidSDKVersionId();
+	static void setCurrentGame(const Common::String &target);
 
 	static inline bool haveSurface();
 	static inline bool swapBuffers();
@@ -161,6 +162,7 @@ private:
 	static jmethodID _MID_getScummVMBasePath;
 	static jmethodID _MID_getScummVMConfigPath;
 	static jmethodID _MID_getScummVMLogPath;
+	static jmethodID _MID_setCurrentGame;
 	static jmethodID _MID_getSysArchives;
 	static jmethodID _MID_getAllStorageLocations;
 	static jmethodID _MID_initSurface;
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVM.java b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
index d9208e298c3..c47db1f380c 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVM.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
@@ -86,6 +86,7 @@ public abstract class ScummVM implements SurfaceHolder.Callback, Runnable {
 	abstract protected String getScummVMBasePath();
 	abstract protected String getScummVMConfigPath();
 	abstract protected String getScummVMLogPath();
+	abstract protected void setCurrentGame(String target);
 	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 483b0dd850b..30c90b32d0a 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -61,7 +61,6 @@ import java.util.Map;
 import java.util.TreeSet;
 
 public class ScummVMActivity extends Activity implements OnKeyboardVisibilityListener {
-
 	/* Establish whether the hover events are available */
 	private static boolean _hoverAvailable;
 
@@ -839,6 +838,18 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 			} else return "";
 		}
 
+		@Override
+		protected void setCurrentGame(String target) {
+			Uri data = null;
+			if (target != null) {
+				data = Uri.fromParts("scummvm", target, null);
+			}
+			Intent intent = new Intent(Intent.ACTION_MAIN, data,
+				ScummVMActivity.this, ScummVMActivity.class);
+			setIntent(intent);
+			Log.d(ScummVM.LOG_TAG, "Current activity Intent is: " + data);
+		}
+
 		@Override
 		protected String[] getSysArchives() {
 			Log.d(ScummVM.LOG_TAG, "Adding to Search Archive: " + _actualScummVMDataDir.getPath());
@@ -908,7 +919,7 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
-//		Log.d(ScummVM.LOG_TAG, "onCreate");
+//		Log.d(ScummVM.LOG_TAG, "onCreate: " + getIntent().getData());
 
 		super.onCreate(savedInstanceState);
 
@@ -1009,9 +1020,19 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 		// We should have a valid path to a configuration file here
 
 		// Start ScummVM
-		_scummvm.setArgs(new String[]{
-			"ScummVM"
-		});
+		final Uri intentData = getIntent().getData();
+		String[] args;
+		if (intentData == null) {
+			args = new String[]{
+				"ScummVM"
+			};
+		} else {
+			args = new String[]{
+				"ScummVM",
+				intentData.getSchemeSpecificPart()
+			};
+		}
+		_scummvm.setArgs(args);
 
 		Log.d(ScummVM.LOG_TAG, "Hover available: " + _hoverAvailable);
 		_mouseHelper = null;
@@ -1054,6 +1075,64 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 		super.onStart();
 	}
 
+	@Override
+	protected void onNewIntent(Intent intent) {
+//		Log.d(ScummVM.LOG_TAG, "onNewIntent: " + intent.getData());
+
+		super.onNewIntent(intent);
+
+		Uri intentData = intent.getData();
+
+		// No specific game, we just continue
+		if (intentData == null) {
+			return;
+		}
+
+		// Same game requested, we continue too
+		if (intentData.equals(getIntent().getData())) {
+			return;
+		}
+
+		setIntent(intent);
+
+		if (_events == null) {
+			finish();
+			startActivity(intent);
+			return;
+		}
+
+		// Don't finish our activity on C++ end
+		_finishing = true;
+
+		_events.clearEventHandler();
+		_events.sendQuitEvent();
+
+		// Make sure the thread is actively polling for events
+		_scummvm.setPause(false);
+		try {
+			// 1s timeout
+			_scummvm_thread.join(2000);
+		} catch (InterruptedException e) {
+			Log.i(ScummVM.LOG_TAG, "Error while joining ScummVM thread", e);
+		}
+
+		// Our join failed: kill ourselves to not have two ScummVM running at the same time
+		if (_scummvm_thread.isAlive()) {
+			Process.killProcess(Process.myPid());
+		}
+
+		_finishing = false;
+
+		String[] args = new String[]{
+			"ScummVM",
+			intentData.getSchemeSpecificPart()
+		};
+		_scummvm.setArgs(args);
+
+		_scummvm_thread = new Thread(_scummvm, "ScummVM");
+		_scummvm_thread.start();
+	}
+
 	@Override
 	public void onResume() {
 //		Log.d(ScummVM.LOG_TAG, "onResume");
diff --git a/backends/platform/android/org/scummvm/scummvm/SplashActivity.java b/backends/platform/android/org/scummvm/scummvm/SplashActivity.java
index 91d53fd4ecb..abe6a526e80 100644
--- a/backends/platform/android/org/scummvm/scummvm/SplashActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/SplashActivity.java
@@ -40,7 +40,9 @@ public class SplashActivity extends Activity {
 			// and they are automatically denied -- onRequestPermissionsResult() will be called without user's input
 			requestPermissions(MY_PERMISSIONS_STR_LIST, MY_PERMISSION_ALL);
 		} else {
-			startActivity(new Intent(this, ScummVMActivity.class));
+			Intent next = new Intent(this, ScummVMActivity.class);
+			next.fillIn(getIntent(), Intent.FILL_IN_ACTION | Intent.FILL_IN_DATA);
+			startActivity(next);
 			finish();
 		}
 	}


Commit: 81669642da323460827e65b9fb1509756c962a32
    https://github.com/scummvm/scummvm/commit/81669642da323460827e65b9fb1509756c962a32
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Add getPath method to INIParser

This mimics the getPath method of ConfigManager

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 381dddca3a9..3aed5e06660 100644
--- a/backends/platform/android/org/scummvm/scummvm/INIParser.java
+++ b/backends/platform/android/org/scummvm/scummvm/INIParser.java
@@ -3,6 +3,7 @@ package org.scummvm.scummvm;
 import android.util.Log;
 
 import java.io.BufferedReader;
+import java.io.File;
 import java.io.IOException;
 import java.io.Reader;
 import java.util.HashMap;
@@ -117,6 +118,50 @@ public class INIParser {
 		return value;
 	}
 
+	public static File getPath(Map<String, Map<String, String>> ini, String section, String key, File defaultValue) {
+		if (ini == null) {
+			return defaultValue;
+		}
+		Map<String, String> s = ini.get(section);
+		if (s == null) {
+			return defaultValue;
+		}
+		String value = s.get(key);
+		if (value == null) {
+			return defaultValue;
+		}
+		// Path components are escaped and puny encoded, undo this
+		File path = new File(""); // Create an abstract empty path
+		for(String component : value.split("/")) {
+			component = decodePathComponent(component);
+			path = new File(path, component);
+		}
+		return path;
+	}
+
+	private static String decodePathComponent(String component) {
+		if (!component.startsWith("xn--")) {
+			return component;
+		}
+
+		String decoded = punycodeDecode(component);
+		if (component == decoded) {
+			return component;
+		}
+
+		StringBuilder result = new StringBuilder(decoded);
+		int i = result.indexOf("\u0081");
+		while(i != -1 && i + 1 < result.length()) {
+			char c = decoded.charAt(i + 1);
+			if (c != 0x79) {
+				result.setCharAt(i, (char)(c - 0x80));
+			}
+			result.deleteCharAt(i + 1);
+			i = result.indexOf("\u0081", i + 1);
+		}
+		return result.toString();
+	}
+
 	/* Java isWhitespace is more inclusive than C one */
 	private static boolean isSpace(char c) {
 		return (c == ' ' || c == '\f' || c == '\n' || c == '\r' || c == '\t' || c == '\013');
@@ -137,4 +182,165 @@ public class INIParser {
 		}
 		return s.substring(begin, end + 1);
 	}
+
+	// punycode parameters, see https://datatracker.ietf.org/doc/html/rfc3492#section-5
+	private static final int BASE         = 36;
+	private static final int TMIN         = 1;
+	private static final int TMAX         = 26;
+	private static final int SKEW         = 38;
+	private static final int DAMP         = 700;
+	private static final int INITIAL_N    = 0x80;
+	private static final int INITIAL_BIAS = 72;
+	private static final int SMAX         = 2147483647; // maximum Unicode code point
+
+	private static String punycodeDecode(String src) {
+		// Check for prefix
+		if (!src.startsWith("xn--")) {
+			return src;
+		}
+
+		// Check if it is ASCII
+		for (int i = 0; i < src.length(); i++) {
+			int c = src.charAt(i);
+			if (c > 0x7F) {
+				return src;
+			}
+		}
+
+		src = src.substring(4);
+
+		int tail = src.length();
+		int startInsert = src.lastIndexOf('-', tail) + 1;
+		while(true) {
+			// Check the insertions string and chop off invalid characters
+			int i;
+			for(i = startInsert; i < tail; i++) {
+				char c = src.charAt(i);
+				if (!((c >= '0' && c <= '9') ||
+					(c >= 'a' && c <= 'z') ||
+					(c >= 'A' && c <= 'Z'))) {
+					break;
+				}
+			}
+			if (i == tail) {
+				// All good
+				break;
+			}
+			if (src.charAt(i) == '.') {
+				// Assume it's an extension, stop there
+				tail = i;
+				break;
+			}
+
+			if (startInsert == 0) {
+				// That was all invalid
+				return src;
+			}
+
+			// Look for previous dash
+			tail = startInsert;
+			startInsert = src.lastIndexOf('-', tail) + 1;
+			// Check again
+		}
+
+		// Do punycode work
+		StringBuilder dest = new StringBuilder(src.substring(0, startInsert > 0 ? startInsert - 1 : 0));
+
+		int di = dest.length();
+		int i = 0;
+		int n = INITIAL_N;
+		int bias = INITIAL_BIAS;
+
+		for (int si = startInsert; si < tail; di++) {
+			int org_i = i;
+
+			for (int w = 1, k = BASE; true; k += BASE) {
+				if (si >= tail) {
+					Log.w(LOG_TAG, "punycode_decode: incorrect digit for string: " + src);
+					return src;
+				}
+
+				int digit = decodeDigit(src.charAt(si));
+				si++;
+
+				if (digit == SMAX) {
+					Log.w(LOG_TAG, "punycode_decode: incorrect digit2 for string: " + src);
+					return src;
+				}
+
+				if (digit > (SMAX - i) / w) {
+					// OVERFLOW
+					Log.w(LOG_TAG, "punycode_decode: overflow1 for string: " + src);
+					return src;
+				}
+
+				i += digit * w;
+				int t;
+
+				if (k <= bias) {
+					t = TMIN;
+				} else if (k >= bias + TMAX) {
+					t = TMAX;
+				} else {
+					t = k - bias;
+				}
+
+				if (digit < t) {
+					break;
+				}
+
+				if (w > SMAX / (BASE - t)) {
+					// OVERFLOW
+					Log.w(LOG_TAG, "punycode_decode: overflow2 for string: "+ src);
+					return src;
+				}
+
+				w *= BASE - t;
+			}
+
+			bias = adaptBias(i - org_i, di + 1, org_i == 0);
+
+			if (i / (di + 1) > SMAX - n) {
+				// OVERFLOW
+				Log.w(LOG_TAG, "punycode_decode: overflow3 for string: " + src);
+				return src;
+			}
+
+			n += i / (di + 1);
+			i %= (di + 1);
+
+			dest.insert(i, Character.toChars(n));
+			i++;
+		}
+
+		// Re-add tail
+		dest.append(src.substring(tail));
+		return dest.toString();
+	}
+
+	private static int decodeDigit(char c) {
+		if (c >= '0' && c <= '9') {
+			return 26 + c - '0';
+		} else if (c >= 'a' && c <= 'z') {
+			return c - 'a';
+		} else if (c >= 'A' && c <= 'Z') {
+			return c - 'A';
+		} else {
+			return SMAX;
+		}
+	}
+
+	private static int adaptBias(int delta, int nPoints, boolean isFirst) {
+		int k;
+
+		delta /= isFirst ? DAMP : 2;
+		delta += delta / nPoints;
+
+		// while delta > 455: delta /= 35
+		for (k = 0; delta > ((BASE - TMIN) * TMAX) / 2; k += BASE) {
+			delta /= (BASE - TMIN);
+		}
+
+		return k + (((BASE - TMIN + 1) * delta) / (delta + SKEW));
+	}
 }


Commit: 1a76acc938af797f0c28e05aa34d00671e61b84e
    https://github.com/scummvm/scummvm/commit/1a76acc938af797f0c28e05aa34d00671e61b84e
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Allow sorting of SAFFSNode using their path component

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


diff --git a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
index 7fe93383e70..e4acf831d45 100644
--- a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
+++ b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
@@ -74,7 +74,7 @@ public class SAFFSTree {
 		}
 	}
 
-	public static class SAFFSNode {
+	public static class SAFFSNode implements Comparable<SAFFSNode> {
 		public static final int DIRECTORY = 0x01;
 		public static final int WRITABLE  = 0x02;
 		public static final int READABLE  = 0x04;
@@ -121,6 +121,14 @@ public class SAFFSTree {
 			}
 			return ourFlags;
 		}
+
+		@Override
+		public int compareTo(SAFFSNode o) {
+			if (o == null) {
+				throw new NullPointerException();
+			}
+			return _path.compareTo(o._path);
+		}
 	}
 
 	private Context _context;


Commit: c1827fd420e38a55583498dae71b8cf49dba5037
    https://github.com/scummvm/scummvm/commit/c1827fd420e38a55583498dae71b8cf49dba5037
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Add a method to get a ParcelFileDescriptor from a SAFFSNode

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


diff --git a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
index e4acf831d45..942abf52e54 100644
--- a/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
+++ b/backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
@@ -439,7 +439,7 @@ public class SAFFSTree {
 		return newnode;
 	}
 
-	private int createStream(SAFFSNode node, String mode) {
+	public ParcelFileDescriptor createFileDescriptor(SAFFSNode node, String mode) {
 		final ContentResolver resolver = _context.getContentResolver();
 		final Uri uri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
 
@@ -447,12 +447,17 @@ public class SAFFSTree {
 		try {
 			pfd = resolver.openFileDescriptor(uri, mode);
 		} catch(FileNotFoundException e) {
-			return -1;
+			return null;
 		}
+
+		return pfd;
+	}
+
+	private int createStream(SAFFSNode node, String mode) {
+		ParcelFileDescriptor pfd = createFileDescriptor(node, mode);
 		if (pfd == null) {
 			return -1;
 		}
-
 		return pfd.detachFd();
 	}
 


Commit: 960c74ad6ae6e6b9baa99a7b46a2d4fed82d8612
    https://github.com/scummvm/scummvm/commit/960c74ad6ae6e6b9baa99a7b46a2d4fed82d8612
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Add compatibility shims to handle shortcuts and drawables

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


diff --git a/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java b/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
index 7f1b8df1910..3b21f7eeeec 100644
--- a/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
+++ b/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
@@ -1,5 +1,16 @@
 package org.scummvm.scummvm;
 
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
 import android.media.AudioAttributes;
 import android.media.AudioFormat;
 import android.media.AudioManager;
@@ -10,8 +21,13 @@ import android.view.WindowInsets;
 import android.view.WindowInsetsController;
 import android.view.accessibility.AccessibilityEvent;
 
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
+import java.util.Objects;
+
 class CompatHelpers {
 	static class HideSystemStatusBar {
 
@@ -146,7 +162,6 @@ class CompatHelpers {
 			} else {
 				return AccessibilityEventConstructorOld.make(eventType);
 			}
-
 		}
 
 		@SuppressWarnings("deprecation")
@@ -162,7 +177,103 @@ class CompatHelpers {
 				return new AccessibilityEvent(eventType);
 			}
 		}
+	}
+
+	static class ShortcutCreator {
+		public static Intent createShortcutResultIntent(@NonNull Context context, String id, @NonNull Intent intent, @NonNull String label, @Nullable Drawable icon, @DrawableRes int fallbackIconId) {
+			if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+				return ShortcutCreatorO.createShortcutResultIntent(context, id, intent, label, icon, fallbackIconId);
+			} else {
+				return ShortcutCreatorOld.createShortcutResultIntent(context, id, intent, label, icon, fallbackIconId);
+			}
+		}
+
+		@SuppressWarnings("deprecation")
+		private static class ShortcutCreatorOld {
+			public static Intent createShortcutResultIntent(@NonNull Context context, String ignoredId, @NonNull Intent intent, @NonNull String label, @Nullable Drawable icon, @DrawableRes int fallbackIconId) {
+				Intent result = new Intent();
+
+				if (icon == null) {
+					icon = DrawableCompat.getDrawable(context, fallbackIconId);
+				}
+				Objects.requireNonNull(icon);
+				Bitmap bmIcon = drawableToBitmap(icon);
+
+				addToIntent(result, intent, label, bmIcon);
+				return result;
+			}
+
+			public static void addToIntent(Intent result, @NonNull Intent intent, @NonNull String label, @NonNull Bitmap icon) {
+				result.putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent);
+				result.putExtra(Intent.EXTRA_SHORTCUT_NAME, label);
+				result.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
+			}
+		}
+
+		@RequiresApi(android.os.Build.VERSION_CODES.O)
+		private static class ShortcutCreatorO {
+			public static Intent createShortcutResultIntent(Context context, String id, @NonNull Intent intent, @NonNull String label, @Nullable Drawable icon, @DrawableRes int fallbackIconId) {
+				ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
+				ShortcutInfo.Builder builder = new ShortcutInfo.Builder(context, id);
+				builder.setIntent(intent);
+				builder.setShortLabel(label);
+				Bitmap bm;
+				if (icon != null) {
+					bm = drawableToBitmap(icon);
+					builder.setIcon(Icon.createWithBitmap(bm));
+				} else {
+					icon = DrawableCompat.getDrawable(context, fallbackIconId);
+					Objects.requireNonNull(icon);
+					bm = drawableToBitmap(icon);
+					builder.setIcon(Icon.createWithResource(context, fallbackIconId));
+				}
+				Intent result = shortcutManager.createShortcutResultIntent(builder.build());
+				ShortcutCreatorOld.addToIntent(result, intent, label, bm);
+				return result;
+			}
+		}
+
+		private static Bitmap drawableToBitmap(@NonNull Drawable drawable) {
+			// We resize to 128x128 to avoid having too big bitmaps for Binder
+			if (drawable instanceof BitmapDrawable) {
+				Bitmap bm = ((BitmapDrawable)drawable).getBitmap();
+				bm = Bitmap.createScaledBitmap(bm, 128, 128, true);
+				return bm.copy(bm.getConfig(), false);
+			}
 
+			Bitmap bitmap = Bitmap.createBitmap(128, 128, Bitmap.Config.ARGB_8888);
+			Canvas canvas = new Canvas(bitmap);
+			drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+			drawable.draw(canvas);
 
+			// Create an immutable bitmap
+			return bitmap.copy(bitmap.getConfig(), false);
+		}
+	}
+
+	static class DrawableCompat {
+		public static Drawable getDrawable(@NonNull Context context, @DrawableRes int id) throws Resources.NotFoundException {
+			if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+				return DrawableCompatLollipop.getDrawable(context, id);
+			} else {
+				return DrawableCompatOld.getDrawable(context, id);
+			}
+		}
+
+		@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP)
+		private static class DrawableCompatLollipop {
+			@SuppressLint("UseCompatLoadingForDrawables")
+			public static Drawable getDrawable(@NonNull Context context, @DrawableRes int id) throws Resources.NotFoundException {
+				return context.getDrawable(id);
+			}
+		}
+
+		@SuppressWarnings("deprecation")
+		private static class DrawableCompatOld {
+			@SuppressLint("UseCompatLoadingForDrawables")
+			public static Drawable getDrawable(@NonNull Context context, @DrawableRes int id) throws Resources.NotFoundException {
+				return context.getResources().getDrawable(id);
+			}
+		}
 	}
 }


Commit: e70b7584966c03792a5ff3b19e2f767aa94d0e6c
    https://github.com/scummvm/scummvm/commit/e70b7584966c03792a5ff3b19e2f767aa94d0e6c
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Add UI to add game shortcuts on the launcher

The UI is started using the standard "Add widget" feature from the
launcher.
Loading of game icons is quite slow and could be optimized later.

Changed paths:
  A backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
  A dists/android/res/drawable/ic_no_game_icon.xml
  A dists/android/res/layout/shortcut_creator_activity.xml
  A dists/android/res/layout/shortcut_creator_customize.xml
  A dists/android/res/layout/shortcut_creator_game_list_item.xml
  A dists/android/res/values-v29/themes.xml
    dists/android.strings.xml.cpp
    dists/android/AndroidManifest.xml
    dists/android/res/values/strings.xml
    dists/android/res/values/themes.xml


diff --git a/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java b/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
new file mode 100644
index 00000000000..106701296a2
--- /dev/null
+++ b/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
@@ -0,0 +1,501 @@
+package org.scummvm.scummvm;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipInputStream;
+
+public class ShortcutCreatorActivity extends Activity {
+	private IconsCache _cache;
+	private GameAdapter _listAdapter;
+
+	private final ExecutorService _executor = new ThreadPoolExecutor(
+		0, Runtime.getRuntime().availableProcessors(),
+		1L, TimeUnit.SECONDS,
+		new LinkedBlockingQueue<>());
+
+	private ProgressBar _progressBar;
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		setContentView(R.layout.shortcut_creator_activity);
+
+		// We are only here to create a shortcut
+		if (!Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())) {
+			finish();
+			return;
+		}
+
+		List<Game> games;
+		Map<String, Map<String, String>> parsedIniMap;
+		try (FileReader reader = new FileReader(new File(getFilesDir(), "scummvm.ini"))) {
+			parsedIniMap = INIParser.parse(reader);
+		} catch(FileNotFoundException ignored) {
+			parsedIniMap = null;
+		} catch(IOException ignored) {
+			parsedIniMap = null;
+		}
+
+		if (parsedIniMap == null) {
+			Toast.makeText(this, R.string.ini_parsing_error, Toast.LENGTH_LONG).show();
+			finish();
+			return;
+		}
+
+		games = Game.loadGames(parsedIniMap);
+
+		OpenFileResult defaultStream = openFile(new File(getFilesDir(), "gui-icons.dat"));
+
+		File iconsPath = INIParser.getPath(parsedIniMap, "scummvm", "iconspath",
+			new File(getFilesDir(), "icons"));
+		OpenFileResult[] packsStream = openFiles(iconsPath, "gui-icons.*\\.dat");
+
+		_cache = new IconsCache(this, _cacheListener,
+			games, defaultStream, packsStream,
+			_executor, new Handler(Looper.getMainLooper()));
+		_listAdapter = new GameAdapter(this, games, _cache);
+
+		ListView listView = findViewById(R.id.shortcut_creator_games_list);
+		listView.setAdapter(_listAdapter);
+		listView.setEmptyView(findViewById(R.id.shortcut_creator_games_list_empty));
+		listView.setOnItemClickListener(_gameClicked);
+
+		_progressBar = findViewById(R.id.shortcut_creator_progress_bar);
+
+		setResult(RESULT_CANCELED);
+	}
+
+	@Override
+	protected void onDestroy() {
+		super.onDestroy();
+
+		_executor.shutdownNow();
+	}
+
+	private static class OpenFileResult {
+		@NonNull
+		public FileInputStream stream;
+		public long streamSize;
+		OpenFileResult(@NonNull FileInputStream stream, long streamSize) {
+			this.stream = stream;
+			this.streamSize = streamSize;
+		}
+	}
+
+	private OpenFileResult openFile(File path) {
+		 try {
+			FileInputStream stream = new FileInputStream(path);
+			return new OpenFileResult(stream, path.length());
+		} catch (FileNotFoundException e) {
+			return null;
+		}
+	}
+
+	private OpenFileResult[] openFiles(File basePath, String regex) {
+		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
+			!basePath.getPath().startsWith("/saf/")) {
+			// This is a standard filesystem path
+			File[] children = basePath.listFiles((dir, name) -> name.matches(regex));
+			if (children == null) {
+				return new OpenFileResult[0];
+			}
+			Arrays.sort(children);
+			OpenFileResult[] ret = new OpenFileResult[children.length];
+			int i = 0;
+			for (File f: children) {
+				ret[i] = openFile(f);
+				i += 1;
+			}
+			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(this, treeName);
+		if (tree == null) {
+			return new OpenFileResult[0];
+		}
+		SAFFSTree.SAFFSNode node = tree.pathToNode(path);
+		if (node == null) {
+			return new OpenFileResult[0];
+		}
+		SAFFSTree.SAFFSNode[] children = tree.getChildren(node);
+		if (children == null) {
+			return new OpenFileResult[0];
+		}
+		Arrays.sort(children);
+
+		ArrayList<OpenFileResult> ret = new ArrayList<>();
+		for (SAFFSTree.SAFFSNode child : children) {
+			if ((child._flags & SAFFSTree.SAFFSNode.DIRECTORY) != 0) {
+				continue;
+			}
+			String component = child._path.substring(child._path.lastIndexOf('/') + 1);
+			if (!component.matches(regex)) {
+				continue;
+			}
+			ParcelFileDescriptor pfd = tree.createFileDescriptor(child, "r");
+			if (pfd == null) {
+				continue;
+			}
+			ret.add(new OpenFileResult(
+				new ParcelFileDescriptor.AutoCloseInputStream(pfd),
+				pfd.getStatSize()
+			));
+		}
+		return ret.toArray(new OpenFileResult[0]);
+	}
+
+	private final OnItemClickListener _gameClicked = new OnItemClickListener() {
+		@Override
+		public void onItemClick(AdapterView<?> a, View v, int position, long id) {
+			Game game = (Game)a.getItemAtPosition(position);
+			final Drawable icon = _cache.getGameIcon(game);
+
+			// Display a customization dialog to let the user change (shorten?) the title
+			AlertDialog.Builder builder = new AlertDialog.Builder(ShortcutCreatorActivity.this);
+			builder.setTitle("Title");
+			View fragment = LayoutInflater.from(ShortcutCreatorActivity.this).inflate(R.layout.shortcut_creator_customize, null);
+			final EditText desc = fragment.findViewById(R.id.shortcut_creator_customize_game_description);
+			desc.setText(game.getDescription());
+			final ImageView iconView = fragment.findViewById(R.id.shortcut_creator_customize_game_icon);
+			Drawable displayedIcon = icon;
+			if (displayedIcon == null) {
+				displayedIcon = CompatHelpers.DrawableCompat.getDrawable(ShortcutCreatorActivity.this, R.drawable.ic_no_game_icon);
+			}
+			iconView.setImageDrawable(displayedIcon);
+			builder.setView(fragment);
+
+			builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
+				dialog.dismiss();
+
+				Intent shortcut = new Intent(Intent.ACTION_MAIN, Uri.fromParts("scummvm", game.getTarget(), null),
+					ShortcutCreatorActivity.this, SplashActivity.class);
+				Intent result = CompatHelpers.ShortcutCreator.createShortcutResultIntent(ShortcutCreatorActivity.this, game.getTarget(), shortcut,
+					desc.getText().toString(), icon, R.drawable.ic_no_game_icon);
+				setResult(RESULT_OK, result);
+
+				finish();
+			});
+			builder.setNegativeButton(android.R.string.cancel, (dialog, which) ->
+				dialog.cancel());
+
+			final AlertDialog dialog = builder.create();
+			desc.setOnEditorActionListener((TextView tv, int actionId, KeyEvent event) -> {
+				if (actionId == EditorInfo.IME_ACTION_DONE) {
+					dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
+					return true;
+				}
+				return false;
+			});
+
+			dialog.show();
+		}
+	};
+
+	private final IconsCache.IconsCacheListener _cacheListener = new IconsCache.IconsCacheListener() {
+		@Override
+		public void onIconUpdated(List<Game> games) {
+			_listAdapter.notifyDataSetChanged();
+		}
+
+		@Override
+		public void onLoadProgress(int percent) {
+			if (percent == 100) {
+				_progressBar.setVisibility(View.GONE);
+			}
+			_progressBar.setProgress(percent);
+		}
+	};
+
+	private static class Game {
+		@NonNull
+		private final String _target;
+		private final String _engineid;
+		private final String _gameid;
+		@NonNull
+		private final String _description;
+
+		private Game(@NonNull String target, String engineid, String gameid, @NonNull String description) {
+			_target = target;
+			_engineid = engineid;
+			_gameid = gameid;
+			_description = description;
+		}
+
+		@NonNull
+		public String getTarget() {
+			return _target;
+		}
+
+		@NonNull
+		public String getDescription() {
+			return _description;
+		}
+
+		public Collection<String> getIconCandidates() {
+			if (_engineid == null) {
+				return new ArrayList<>();
+			}
+
+			ArrayList<String> ret = new ArrayList<>();
+			if (_gameid != null) {
+				ret.add(String.format("icons/%s-%s.png", _engineid, _gameid).toLowerCase());
+			}
+			ret.add(String.format("icons/%s.png", _engineid).toLowerCase());
+
+			return ret;
+		}
+
+		public static List<Game> loadGames(@NonNull Map<String, Map<String, String>> parsedIniMap) {
+			List<Game> games = new ArrayList<>();
+			for (Map.Entry<String, Map<String, String>> entry : parsedIniMap.entrySet()) {
+				final String domain = entry.getKey();
+				if (domain == null ||
+					"scummvm".equals(domain) ||
+					"cloud".equals(domain) ||
+					"keymapper".equals(domain)) {
+					continue;
+				}
+				String engineid = entry.getValue().get("engineid");
+				String gameid = entry.getValue().get("gameid");
+				String description = entry.getValue().get("description");
+				if (description == null) {
+					continue;
+				}
+				games.add(new Game(domain, engineid, gameid, description));
+			}
+			return games;
+		}
+
+		@NonNull
+		@Override
+		public String toString() {
+			return _description;
+		}
+	}
+
+	private static class IconsCache {
+		/**
+		 * This kind of mimics Common::generateZipSet with asynchronous feature
+		 */
+		public interface IconsCacheListener {
+			void onIconUpdated(List<Game> games);
+			void onLoadProgress(int percent);
+		}
+
+		private final Context _context;
+		private final IconsCacheListener _listener;
+		private final Map<String, byte[]> _icons = new HashMap<>();
+		private final Map<String, List<Game>> _candidates = new HashMap<>();
+
+		private long _totalSize;
+		private final long[] _totalSizes;
+		private final long[] _readSizes;
+
+		public IconsCache(Context context, IconsCacheListener listener,
+		                  List<Game> games,
+		                  OpenFileResult defaultStream, OpenFileResult[] packsStream,
+		                  Executor executor, Handler handler) {
+			_context = context;
+			_listener = listener;
+
+			// Establish a list of candidates
+			for (Game game : games) {
+				for (String candidate : game.getIconCandidates()) {
+					List<Game> v = _candidates.get(candidate);
+					if (v == null) {
+						v = new ArrayList<>();
+						_candidates.put(candidate, v);
+					}
+					v.add(game);
+				}
+			}
+
+			_totalSizes = new long[1 + packsStream.length];
+			_readSizes = new long[1 + packsStream.length];
+
+			// Iterate over the files starting with default and continuing with packs
+			// This will let us erase outdated versions
+			if (defaultStream != null) {
+				_totalSizes[0] = defaultStream.streamSize;
+				_totalSize += _totalSizes[0];
+				executor.execute(() -> loadZip(defaultStream.stream, 0, handler));
+			}
+			int i = 1;
+			for (final OpenFileResult packStream : packsStream) {
+				if (packStream == null) {
+					continue;
+				}
+
+				_totalSizes[i] = packStream.streamSize;
+				_totalSize += _totalSizes[i];
+				// Make it final for lambda
+				int argI = i;
+				executor.execute(() -> loadZip(packStream.stream, argI, handler));
+				i += 1;
+			}
+
+			if (_totalSize == 0) {
+				handler.post(() -> _listener.onLoadProgress(100));
+			}
+		}
+
+		public Drawable getGameIcon(Game game) {
+			for (String name : game.getIconCandidates()) {
+				byte[] data = _icons.get(name);
+				if (data == null) {
+					continue;
+				}
+
+				Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+				if (bitmap == null) {
+					continue;
+				}
+
+				return new BitmapDrawable(_context.getResources(), bitmap);
+			}
+
+			return null;
+		}
+
+		private void loadZip(@NonNull FileInputStream zipStream, int id, @NonNull Handler handler) {
+			try (ZipInputStream zip = new ZipInputStream(zipStream)) {
+				ZipEntry entry;
+				while ((entry = zip.getNextEntry()) != null) {
+					_readSizes[id] = zipStream.getChannel().position();
+					handler.post(() -> {
+						long readSize = 0;
+						for (long pos : _readSizes) {
+							readSize += pos;
+						}
+						_listener.onLoadProgress((int)(readSize * 100 / _totalSize));
+					});
+
+					String name = entry.getName().toLowerCase();
+					if (entry.isDirectory()) {
+						zip.closeEntry();
+						continue;
+					}
+					if (!_candidates.containsKey(name)) {
+						zip.closeEntry();
+						continue;
+					}
+
+					int sz = (int) entry.getSize();
+					byte[] buffer = new byte[sz];
+					int off = 0;
+					while (off < buffer.length && (sz = zip.read(buffer, off, buffer.length - off)) > 0) {
+						off += sz;
+					}
+					if (off != buffer.length) {
+						throw new IOException();
+					}
+
+					_icons.put(name, buffer);
+					handler.post(() -> _listener.onIconUpdated(_candidates.get(name)));
+
+					zip.closeEntry();
+				}
+				_readSizes[id] = _totalSizes[id];
+				handler.post(() -> {
+					long readSize = 0;
+					for (long pos : _readSizes) {
+						readSize += pos;
+					}
+					_listener.onLoadProgress((int)(readSize * 100 / _totalSize));
+				});
+			} catch (ZipException ignored) {
+			} catch (IOException ignored) {
+			}
+		}
+	}
+
+	private static class GameAdapter extends ArrayAdapter<Game> {
+		private final IconsCache _cache;
+
+		public GameAdapter(Context context,
+		                   List<Game> items,
+		                   IconsCache cache) {
+			super(context, 0, items);
+			Collections.sort(items, (lhs, rhs) -> lhs.getDescription().compareToIgnoreCase(rhs.getDescription()));
+			_cache = cache;
+		}
+
+		@NonNull
+		@Override
+		public View getView(int position, View convertView, @NonNull ViewGroup parent)
+		{
+			if (convertView == null) {
+				convertView = LayoutInflater.from(getContext()).inflate(R.layout.shortcut_creator_game_list_item, parent, false);
+			}
+			final Game game = getItem(position);
+			assert game != null;
+
+			final TextView desc = convertView.findViewById(R.id.shortcut_creator_game_item_description);
+			desc.setText(game.getDescription());
+			final ImageView iconView = convertView.findViewById(R.id.shortcut_creator_game_item_icon);
+			Drawable icon = _cache.getGameIcon(game);
+			if (icon == null) {
+				icon = CompatHelpers.DrawableCompat.getDrawable(getContext(), R.drawable.ic_no_game_icon);
+			}
+			iconView.setImageDrawable(icon);
+			return convertView;
+		}
+	}
+}
diff --git a/dists/android.strings.xml.cpp b/dists/android.strings.xml.cpp
index 712d1a7223b..41c31b77146 100644
--- a/dists/android.strings.xml.cpp
+++ b/dists/android.strings.xml.cpp
@@ -52,3 +52,9 @@ static Common::U32String customkeyboardview_keycode_enter = _("Enter");
 static Common::U32String customkeyboardview_popup_close = _("Close popup");
 static Common::U32String saf_request_prompt = _("Please select the *root* of your external (physical) SD card. This is required for ScummVM to access this path: ");
 static Common::U32String saf_revoke_done = _("Storage Access Framework Permissions for ScummVM were revoked!");
+static Common::U32String ini_parsing_error = _("Configuration file could not be parsed");
+static Common::U32String shortcut_creator_title = _("Run a game");
+static Common::U32String shortcut_creator_no_games = _("No games (yet)\nOpen ScummVM and add your games here.");
+static Common::U32String shortcut_creator_game_item_icon = _("Game icon");
+static Common::U32String shortcut_creator_customize_game_name_hint = _("Shortcut label");
+static Common::U32String shortcut_creator_customize_game_icon_description = _("Shortcut icon");
diff --git a/dists/android/AndroidManifest.xml b/dists/android/AndroidManifest.xml
index 507b6ca8cf4..07b2de9765b 100644
--- a/dists/android/AndroidManifest.xml
+++ b/dists/android/AndroidManifest.xml
@@ -67,6 +67,16 @@
 			android:launchMode="singleInstance"
 			android:theme="@style/AppTheme"
 			android:windowSoftInputMode="adjustResize" />
+		<activity
+			android:name=".ShortcutCreatorActivity"
+			android:exported="true"
+			android:label="@string/shortcut_creator_title"
+			android:theme="@style/ShortcutCreatorTheme">
+			<intent-filter>
+				<action android:name="android.intent.action.CREATE_SHORTCUT" />
+				<category android:name="android.intent.category.DEFAULT" />
+			</intent-filter>
+		</activity>
 	</application>
 
 </manifest>
diff --git a/dists/android/res/drawable/ic_no_game_icon.xml b/dists/android/res/drawable/ic_no_game_icon.xml
new file mode 100644
index 00000000000..a6552001e28
--- /dev/null
+++ b/dists/android/res/drawable/ic_no_game_icon.xml
@@ -0,0 +1,76 @@
+<vector android:height="108dp" android:viewportHeight="800" android:viewportWidth="800" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <group android:translateX="58" android:translateY="58">
+        <clip-path android:pathData="M0,0L682.667,0L682.667,682.667L0,682.667Z"/>
+        <group>
+            <clip-path android:pathData="M90.251,20.717L605.749,20.717L605.749,675.282L90.251,675.282Z"/>
+            <path android:fillAlpha="0.300003"
+                android:fillColor="#000000" android:fillType="nonZero"
+                android:pathData="m564.444,332.561c32.812,39.115 45.021,110.283 40.343,147.205 -6.977,55.073 -32.001,107.72 -73.272,138.776 -42.943,32.316 -95.469,51.445 -161.029,55.86 -98.625,6.621 -175.324,-24.795 -226.327,-72.353 -26.14,-24.408 -67.189,-59.448 -49.703,-109.175 9.001,-25.591 42.056,-57.22 63.431,-79.107 7.676,-7.861 22.791,-22.423 21.143,-31.957 -3.784,-16.149 -28.551,-40.425 -39.02,-57.648 -31.385,-49.204 -38.396,-123.977 -10.773,-186.119 32.936,-74.069 115.811,-121.145 229.133,-117.083 46.915,1.683 104.068,17.139 143.347,39.731C557.825,92.939 603.492,130.141 597.725,171.113 594.181,196.288 575.475,224.245 553.344,251.204 539.617,267.925 533.243,278.139 534.032,287.951 534.704,296.277 554.988,321.289 564.444,332.561"
+                android:strokeAlpha="0.300003" android:strokeColor="#00000000"/>
+            <path android:fillAlpha="0.300003"
+                android:fillColor="#000000" android:fillType="nonZero"
+                android:pathData="m549.451,157.746l0,23.153l-45.18,68.541l0,-23.152z"
+                android:strokeAlpha="0.300003" android:strokeColor="#00000000"/>
+            <group>
+                <clip-path android:pathData="m504.271,226.288l0,23.153c-2.628,3.988 -6.713,6.079 -10.909,6.079 -3.027,-0 -6.112,-1.089 -8.747,-3.337 -0.568,-0.484 -11.352,-9.667 -23.916,-17.656 -12.564,-7.965 -24.876,-14.772 -36.912,-20.444 -12.048,-5.649 -23.304,-9.904 -33.78,-12.728 -10.476,-2.824 -19.632,-4.255 -27.48,-4.255 -9.432,-0 -16.38,2.317 -20.82,6.949 -4.452,4.632 -6.672,10.541 -6.672,17.751l0,-23.153c0,-7.209 2.22,-13.119 6.672,-17.751 4.44,-4.632 11.388,-6.949 20.82,-6.949 7.848,-0 17.004,1.431 27.48,4.255 10.476,2.825 21.732,7.079 33.78,12.728 12.036,5.672 24.348,12.48 36.912,20.444 12.564,7.989 23.348,17.173 23.916,17.656 2.635,2.248 5.72,3.337 8.747,3.337 4.196,-0 8.281,-2.091 10.909,-6.079"/>
+                <path android:fillAlpha="0.300003"
+                    android:fillColor="#000000"
+                    android:fillType="nonZero"
+                    android:pathData="m504.271,226.288l0,23.153c-0.719,1.089 -1.545,2.039 -2.453,2.84L501.817,229.128C502.725,228.326 503.552,227.377 504.271,226.288"
+                    android:strokeAlpha="0.300003" android:strokeColor="#00000000"/>
+                <path android:fillAlpha="0.300003"
+                    android:fillColor="#000000"
+                    android:fillType="nonZero"
+                    android:pathData="m501.818,229.128l0,23.153c-2.416,2.133 -5.408,3.237 -8.456,3.237 -1.899,-0 -3.82,-0.427 -5.637,-1.301l0,-23.153c1.817,0.873 3.739,1.303 5.637,1.303 3.048,-0 6.04,-1.104 8.456,-3.239"
+                    android:strokeAlpha="0.300003" android:strokeColor="#00000000"/>
+                <path android:fillAlpha="0.300003"
+                    android:fillColor="#000000"
+                    android:fillType="nonZero"
+                    android:pathData="m487.724,231.064l0,23.153c-1.083,-0.52 -2.127,-1.197 -3.109,-2.036 -0.568,-0.483 -11.352,-9.667 -23.916,-17.655 -11.857,-7.519 -23.492,-14.004 -34.881,-19.477l0,-23.153c11.389,5.473 23.024,11.96 34.881,19.477C473.263,219.363 484.047,228.546 484.615,229.03 485.597,229.868 486.641,230.544 487.724,231.064"
+                    android:strokeAlpha="0.300003" android:strokeColor="#00000000"/>
+                <path android:fillAlpha="0.300003"
+                    android:fillColor="#000000"
+                    android:fillType="nonZero"
+                    android:pathData="m425.818,191.897l0,23.153c-0.677,-0.327 -1.355,-0.649 -2.031,-0.967 -12.049,-5.649 -23.304,-9.904 -33.78,-12.728 -10.476,-2.824 -19.632,-4.255 -27.481,-4.255 -8.691,-0 -15.273,1.967 -19.728,5.901L342.798,179.849c4.455,-3.935 11.037,-5.901 19.728,-5.901 7.849,-0 17.005,1.429 27.481,4.255C400.483,181.026 411.738,185.281 423.787,190.929 424.463,191.249 425.14,191.57 425.818,191.897"
+                    android:strokeAlpha="0.300003" android:strokeColor="#00000000"/>
+                <path android:fillAlpha="0.300003"
+                    android:fillColor="#000000"
+                    android:fillType="nonZero"
+                    android:pathData="m342.798,179.848l0,23.153c-0.38,0.335 -0.743,0.684 -1.092,1.048 -4.452,4.632 -6.672,10.541 -6.672,17.749l0,-23.152c0,-7.209 2.22,-13.119 6.672,-17.751C342.055,180.532 342.418,180.183 342.798,179.848"
+                    android:strokeAlpha="0.300003" android:strokeColor="#00000000"/>
+            </group>
+            <group>
+                <clip-path android:pathData="m383.743,460.303l0,23.152c0,-9.773 -5.388,-19.027 -16.104,-27.783 -10.74,-8.747 -24.096,-17.868 -40.068,-27.407 -15.972,-9.513 -33.252,-19.793 -51.84,-30.879 -18.648,-11.109 -36.036,-24.167 -51.828,-38.964 -15.984,-14.913 -29.328,-32.036 -40.068,-51.323 -10.728,-19.299 -16.092,-41.552 -16.092,-66.771l0,-23.153c0,25.219 5.364,47.472 16.092,66.771 10.74,19.287 24.084,36.411 40.068,51.324 15.792,14.796 33.18,27.855 51.828,38.963 18.588,11.085 35.868,21.367 51.84,30.88 15.972,9.536 29.328,18.66 40.068,27.405 10.716,8.756 16.104,18.009 16.104,27.784"/>
+                <path android:fillAlpha="0.300003"
+                    android:fillColor="#000000"
+                    android:fillType="nonZero"
+                    android:pathData="m383.742,460.302l0,23.152c0,-9.773 -5.388,-19.025 -16.104,-27.783 -10.74,-8.745 -24.095,-17.868 -40.067,-27.405 -15.972,-9.513 -33.253,-19.795 -51.841,-30.88 -18.648,-11.109 -36.036,-24.167 -51.828,-38.963 -15.984,-14.915 -29.327,-32.037 -40.068,-51.324 -10.728,-19.297 -16.092,-41.551 -16.092,-66.769l0,-23.153c0,25.219 5.364,47.471 16.092,66.769 10.741,19.287 24.084,36.411 40.068,51.325 15.792,14.795 33.18,27.853 51.828,38.961 18.588,11.087 35.869,21.368 51.841,30.88 15.972,9.537 29.327,18.661 40.067,27.405C378.354,441.274 383.742,450.529 383.742,460.302"
+                    android:strokeAlpha="0.300003" android:strokeColor="#00000000"/>
+            </group>
+            <group>
+                <clip-path android:pathData="m552.607,444.087l0,23.155c0,27.784 -5.892,51.843 -17.676,72.159 -11.784,20.339 -27.096,37.048 -45.948,50.179 -18.84,13.116 -40.2,22.891 -64.008,29.331 -23.832,6.417 -47.796,9.643 -71.868,9.643 -48.18,-0 -88.632,-8.756 -121.356,-26.235C199.027,584.827 169.579,558.32 143.395,522.819L143.395,499.667C169.579,535.167 199.027,561.675 231.751,579.164 264.475,596.641 304.927,605.4 353.107,605.4c24.072,-0 48.036,-3.227 71.868,-9.644 23.808,-6.441 45.168,-16.215 64.008,-29.332 18.852,-13.128 34.164,-29.84 45.948,-50.177 11.784,-20.315 17.676,-44.376 17.676,-72.16"/>
+                <path android:fillAlpha="0.300003"
+                    android:fillColor="#000000"
+                    android:fillType="nonZero"
+                    android:pathData="m552.607,444.087l0,23.153c0,27.784 -5.892,51.844 -17.676,72.159 -8.685,14.991 -19.287,28.009 -31.809,39.076l0,-23.152c12.523,-11.068 23.124,-24.087 31.809,-39.077C546.715,495.933 552.607,471.871 552.607,444.087"
+                    android:strokeAlpha="0.300003" android:strokeColor="#00000000"/>
+                <path android:fillAlpha="0.300003"
+                    android:fillColor="#000000"
+                    android:fillType="nonZero"
+                    android:pathData="m503.121,555.323l0,23.152c-4.468,3.949 -9.181,7.651 -14.139,11.103 -18.839,13.117 -40.2,22.891 -64.007,29.331 -23.832,6.419 -47.797,9.644 -71.869,9.644 -44.78,-0 -82.883,-7.564 -114.312,-22.664l0,-23.153c31.429,15.1 69.532,22.665 114.312,22.665 24.072,-0 48.037,-3.227 71.869,-9.644 23.807,-6.441 45.168,-16.215 64.007,-29.332C493.94,562.971 498.653,559.272 503.121,555.323"
+                    android:strokeAlpha="0.300003" android:strokeColor="#00000000"/>
+                <path android:fillAlpha="0.300003"
+                    android:fillColor="#000000"
+                    android:fillType="nonZero"
+                    android:pathData="m238.795,582.734l0,23.153c-2.387,-1.147 -4.735,-2.337 -7.044,-3.569 -32.724,-17.492 -62.172,-43.999 -88.356,-79.5L143.395,499.666C169.579,535.166 199.027,561.674 231.751,579.164 234.06,580.397 236.408,581.588 238.795,582.734"
+                    android:strokeAlpha="0.300003" android:strokeColor="#00000000"/>
+            </group>
+            <path android:fillAlpha="0.300003"
+                android:fillColor="#000000" android:fillType="nonZero"
+                android:pathData="m342.103,67.446c41.88,-0 79.452,7.599 112.704,22.773 33.252,15.185 64.8,37.699 94.644,67.527l-45.18,68.541c-2.628,3.989 -6.713,6.079 -10.909,6.079 -3.027,-0 -6.112,-1.088 -8.747,-3.336 -0.568,-0.484 -11.352,-9.667 -23.916,-17.656 -12.565,-7.965 -24.876,-14.772 -36.912,-20.445 -12.048,-5.648 -23.304,-9.903 -33.78,-12.727 -10.476,-2.825 -19.632,-4.255 -27.481,-4.255 -9.431,-0 -16.379,2.316 -20.819,6.948 -4.452,4.633 -6.672,10.543 -6.672,17.751 0,9.253 5.364,17.881 16.103,25.857 10.716,7.977 24.205,16.201 40.44,24.699 17.569,9.229 34.993,18.744 52.237,28.552 18.576,10.553 35.987,23.292 52.224,38.207 16.236,14.937 29.724,32.819 40.464,53.652 10.728,20.835 16.104,45.664 16.104,74.475 0,27.784 -5.892,51.845 -17.676,72.159 -11.784,20.339 -27.096,37.049 -45.948,50.179 -18.84,13.117 -40.2,22.891 -64.008,29.332 -23.832,6.416 -47.796,9.643 -71.868,9.643 -48.18,-0 -88.632,-8.757 -121.356,-26.235 -32.724,-17.491 -62.172,-43.997 -88.356,-79.497l55.8,-69.371c2.672,-3.316 6.303,-4.988 9.953,-4.988 3.413,-0 6.844,1.463 9.523,4.409 0.32,0.352 6.408,7.043 17.412,15.54 11.136,8.591 23.112,16.083 35.736,22.371 12.612,6.359 25.74,11.652 39.264,15.824 13.356,4.136 25.788,6.18 37.308,6.18 11.508,-0 20.292,-2.056 26.316,-6.18 6.012,-4.113 9.035,-11.829 9.035,-23.151 0,-9.773 -5.388,-19.027 -16.104,-27.784 -10.739,-8.744 -24.095,-17.868 -40.067,-27.405 -15.972,-9.512 -33.252,-19.793 -51.84,-30.88 -18.648,-11.108 -36.036,-24.167 -51.828,-38.961 -15.984,-14.915 -29.328,-32.039 -40.068,-51.325 -10.728,-19.299 -16.092,-41.551 -16.092,-66.769 0,-24.7 4.704,-46.303 14.136,-64.82 9.42,-18.531 22.116,-34.083 38.088,-46.704 15.972,-12.597 34.428,-22.111 55.368,-28.551C296.275,70.673 318.535,67.446 342.103,67.446"
+                android:strokeAlpha="0.300003" android:strokeColor="#00000000"/>
+        </group>
+        <path android:fillColor="#7f7f7f" android:fillType="nonZero"
+            android:pathData="m557.777,325.895c32.812,39.115 45.021,110.283 40.343,147.205 -6.977,55.073 -32.001,107.72 -73.272,138.776 -42.943,32.316 -95.469,51.445 -161.029,55.86 -98.625,6.621 -175.324,-24.795 -226.327,-72.353 -26.14,-24.408 -67.189,-59.448 -49.703,-109.175 9.001,-25.591 42.056,-57.22 63.431,-79.107 7.676,-7.861 22.791,-22.423 21.143,-31.957 -3.784,-16.149 -28.551,-40.425 -39.02,-57.648 -31.385,-49.204 -38.396,-123.977 -10.773,-186.119 32.936,-74.069 115.811,-121.145 229.133,-117.083 46.915,1.683 104.068,17.139 143.347,39.731C551.159,86.272 596.825,123.475 591.059,164.447 587.515,189.621 568.808,217.579 546.677,244.537 532.951,261.259 526.576,271.472 527.365,281.284 528.037,289.611 548.321,314.623 557.777,325.895" android:strokeColor="#00000000"/>
+    </group>
+</vector>
diff --git a/dists/android/res/layout/shortcut_creator_activity.xml b/dists/android/res/layout/shortcut_creator_activity.xml
new file mode 100644
index 00000000000..e2abbd4d477
--- /dev/null
+++ b/dists/android/res/layout/shortcut_creator_activity.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	android:orientation="vertical"
+	android:paddingLeft="5dp"
+	android:paddingTop="10dp"
+	android:paddingRight="5dp"
+	android:paddingBottom="10dp">
+
+	<FrameLayout
+		android:layout_width="match_parent"
+		android:layout_height="0dp"
+		android:layout_weight="1">
+
+		<ListView
+			android:id="@+id/shortcut_creator_games_list"
+			android:layout_width="match_parent"
+			android:layout_height="match_parent"
+			android:divider="?android:attr/listDivider"
+			android:dividerHeight="1dp"
+			android:scrollbarAlwaysDrawVerticalTrack="true" />
+
+		<TextView
+			android:id="@+id/shortcut_creator_games_list_empty"
+			android:layout_width="match_parent"
+			android:layout_height="match_parent"
+			android:layout_marginStart="15dp"
+			android:layout_marginLeft="15dp"
+			android:gravity="center_horizontal|center_vertical"
+			android:text="@string/shortcut_creator_no_games" />
+
+	</FrameLayout>
+
+
+	<ProgressBar
+		android:id="@+id/shortcut_creator_progress_bar"
+		style="?android:attr/progressBarStyleHorizontal"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:max="100"
+		tools:progress="50" />
+
+
+</LinearLayout>
diff --git a/dists/android/res/layout/shortcut_creator_customize.xml b/dists/android/res/layout/shortcut_creator_customize.xml
new file mode 100644
index 00000000000..66a5ad66103
--- /dev/null
+++ b/dists/android/res/layout/shortcut_creator_customize.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	android:orientation="vertical">
+
+	<ImageView
+		android:id="@+id/shortcut_creator_customize_game_icon"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_gravity="center_horizontal"
+		android:contentDescription="@string/shortcut_creator_customize_game_icon_description"
+		tools:srcCompat="@drawable/ic_no_game_icon" />
+
+	<EditText
+		android:id="@+id/shortcut_creator_customize_game_description"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:autofillHints=""
+		android:hint="@string/shortcut_creator_customize_game_name_hint"
+		android:imeOptions="actionDone"
+		android:inputType="text"
+		android:minHeight="48dp"
+		android:paddingLeft="5dp"
+		android:paddingRight="5dp"
+		android:singleLine="true"
+		tools:text="Sample game" />
+</LinearLayout>
diff --git a/dists/android/res/layout/shortcut_creator_game_list_item.xml b/dists/android/res/layout/shortcut_creator_game_list_item.xml
new file mode 100644
index 00000000000..2b556f234d9
--- /dev/null
+++ b/dists/android/res/layout/shortcut_creator_game_list_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="fill_parent"
+	android:layout_height="wrap_content"
+	android:background="?android:attr/activatedBackgroundIndicator"
+	android:orientation="horizontal"
+	android:paddingBottom="6dip"
+	android:paddingTop="4dip">
+
+	<ImageView
+		android:id="@+id/shortcut_creator_game_item_icon"
+		android:layout_width="54dp"
+		android:layout_height="54dp"
+		android:layout_gravity="start"
+		android:contentDescription="@string/shortcut_creator_game_item_icon"
+		tools:src="@drawable/ic_no_game_icon" />
+
+	<TextView
+		android:id="@+id/shortcut_creator_game_item_description"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"
+		android:layout_marginStart="15dp"
+		android:layout_marginLeft="15dp"
+		android:layout_marginTop="2dp"
+		android:layout_marginEnd="2dp"
+		android:layout_marginRight="2dp"
+		android:layout_marginBottom="2dp"
+		android:ellipsize="end"
+		android:gravity="center_vertical"
+		android:maxLines="2"
+		android:textAppearance="?android:attr/textAppearanceMedium"
+		tools:text="Game description" />
+
+</LinearLayout>
diff --git a/dists/android/res/values-v29/themes.xml b/dists/android/res/values-v29/themes.xml
new file mode 100644
index 00000000000..9eca6f419bb
--- /dev/null
+++ b/dists/android/res/values-v29/themes.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+	<style name="ShortcutCreatorThemeBase" parent="@android:style/Theme.DeviceDefault.DayNight">
+	</style>
+</resources>
diff --git a/dists/android/res/values/strings.xml b/dists/android/res/values/strings.xml
index d54d0e3ce25..360508713bd 100644
--- a/dists/android/res/values/strings.xml
+++ b/dists/android/res/values/strings.xml
@@ -60,4 +60,11 @@
 	<string name="saf_request_prompt">Please select the *root* of your external (physical) SD card. This is required for ScummVM to access this path: </string>
 	<string name="saf_revoke_done">Storage Access Framework Permissions for ScummVM were revoked!</string>
 	<string name="preference_saf_tree_key" translatable="false">pref_key__saf_tree_uri</string>
+
+	<string name="ini_parsing_error">Configuration file could not be parsed</string>
+	<string name="shortcut_creator_title">Run a game</string>
+	<string name="shortcut_creator_no_games">No games (yet)\nOpen ScummVM and add your games here.</string>
+	<string name="shortcut_creator_game_item_icon">Game icon</string>
+	<string name="shortcut_creator_customize_game_name_hint">Shortcut label</string>
+	<string name="shortcut_creator_customize_game_icon_description">Shortcut icon</string>
 </resources>
diff --git a/dists/android/res/values/themes.xml b/dists/android/res/values/themes.xml
index e2a6987d63f..8e553cfb4fb 100644
--- a/dists/android/res/values/themes.xml
+++ b/dists/android/res/values/themes.xml
@@ -10,4 +10,10 @@
 		<item name="android:windowBackground">@drawable/splash</item>
 	</style>
 
+	<!-- This base theme is overridden in values-v29 to allow for Day/Night theme selection -->
+	<style name="ShortcutCreatorThemeBase" parent="@android:style/Theme.DeviceDefault">
+	</style>
+
+	<style name="ShortcutCreatorTheme" parent="ShortcutCreatorThemeBase">
+	</style>
 </resources>


Commit: a329f620690f8a085e88d5e3283e76722e17a900
    https://github.com/scummvm/scummvm/commit/a329f620690f8a085e88d5e3283e76722e17a900
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Add a search field to games list

Changed paths:
    backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
    dists/android.strings.xml.cpp
    dists/android/res/layout/shortcut_creator_activity.xml
    dists/android/res/values/strings.xml


diff --git a/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java b/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
index 106701296a2..2ae006e742e 100644
--- a/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
@@ -15,6 +15,8 @@ import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.ParcelFileDescriptor;
+import android.text.Editable;
+import android.text.TextWatcher;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -111,6 +113,27 @@ public class ShortcutCreatorActivity extends Activity {
 
 		_progressBar = findViewById(R.id.shortcut_creator_progress_bar);
 
+		EditText searchEdit = findViewById(R.id.shortcut_creator_search_edit);
+		searchEdit.addTextChangedListener(new TextWatcher() {
+
+			@Override
+			public void onTextChanged(CharSequence cs, int arg1, int arg2, int arg3) {
+				_listAdapter.getFilter().filter(cs.toString());
+			}
+
+			@Override
+			public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
+										  int arg3) {
+			}
+
+			@Override
+			public void afterTextChanged(Editable arg0) {
+			}
+		});
+		if (games.isEmpty()) {
+			searchEdit.setVisibility(View.GONE);
+		}
+
 		setResult(RESULT_CANCELED);
 	}
 
diff --git a/dists/android.strings.xml.cpp b/dists/android.strings.xml.cpp
index 41c31b77146..5b4e8a2adbb 100644
--- a/dists/android.strings.xml.cpp
+++ b/dists/android.strings.xml.cpp
@@ -54,6 +54,7 @@ static Common::U32String saf_request_prompt = _("Please select the *root* of you
 static Common::U32String saf_revoke_done = _("Storage Access Framework Permissions for ScummVM were revoked!");
 static Common::U32String ini_parsing_error = _("Configuration file could not be parsed");
 static Common::U32String shortcut_creator_title = _("Run a game");
+static Common::U32String shortcut_creator_search_game = _("Search a game");
 static Common::U32String shortcut_creator_no_games = _("No games (yet)\nOpen ScummVM and add your games here.");
 static Common::U32String shortcut_creator_game_item_icon = _("Game icon");
 static Common::U32String shortcut_creator_customize_game_name_hint = _("Shortcut label");
diff --git a/dists/android/res/layout/shortcut_creator_activity.xml b/dists/android/res/layout/shortcut_creator_activity.xml
index e2abbd4d477..988971c1d33 100644
--- a/dists/android/res/layout/shortcut_creator_activity.xml
+++ b/dists/android/res/layout/shortcut_creator_activity.xml
@@ -9,6 +9,20 @@
 	android:paddingRight="5dp"
 	android:paddingBottom="10dp">
 
+	<EditText
+		android:id="@+id/shortcut_creator_search_edit"
+		android:layout_width="match_parent"
+		android:layout_height="48dp"
+		android:drawableLeft="@android:drawable/ic_menu_search"
+		android:drawableStart="@android:drawable/ic_menu_search"
+		android:ems="10"
+		android:hint="@string/shortcut_creator_search_game"
+		android:imeOptions="actionDone"
+		android:importantForAutofill="no"
+		android:inputType="text"
+		android:singleLine="true"
+		tools:ignore="VisualLintTextFieldSize" />
+
 	<FrameLayout
 		android:layout_width="match_parent"
 		android:layout_height="0dp"
@@ -33,7 +47,6 @@
 
 	</FrameLayout>
 
-
 	<ProgressBar
 		android:id="@+id/shortcut_creator_progress_bar"
 		style="?android:attr/progressBarStyleHorizontal"
@@ -42,5 +55,4 @@
 		android:max="100"
 		tools:progress="50" />
 
-
 </LinearLayout>
diff --git a/dists/android/res/values/strings.xml b/dists/android/res/values/strings.xml
index 360508713bd..086ccb0a14e 100644
--- a/dists/android/res/values/strings.xml
+++ b/dists/android/res/values/strings.xml
@@ -63,6 +63,7 @@
 
 	<string name="ini_parsing_error">Configuration file could not be parsed</string>
 	<string name="shortcut_creator_title">Run a game</string>
+	<string name="shortcut_creator_search_game">Search a game</string>
 	<string name="shortcut_creator_no_games">No games (yet)\nOpen ScummVM and add your games here.</string>
 	<string name="shortcut_creator_game_item_icon">Game icon</string>
 	<string name="shortcut_creator_customize_game_name_hint">Shortcut label</string>


Commit: c966d31c216147bb6d4303827bb2de61932b6e35
    https://github.com/scummvm/scummvm/commit/c966d31c216147bb6d4303827bb2de61932b6e35
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Add our own ZipFile implementation

This allows us to use a FileInputStream (as given by SAF) but with
seeking support.
This avoids us to parse the whole zip files while we only need some of
the files.

Changed paths:
  A backends/platform/android/org/scummvm/scummvm/zip/ZipCoder.java
  A backends/platform/android/org/scummvm/scummvm/zip/ZipConstants.java
  A backends/platform/android/org/scummvm/scummvm/zip/ZipConstants64.java
  A backends/platform/android/org/scummvm/scummvm/zip/ZipEntry.java
  A backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java
  A backends/platform/android/org/scummvm/scummvm/zip/ZipUtils.java


diff --git a/backends/platform/android/org/scummvm/scummvm/zip/ZipCoder.java b/backends/platform/android/org/scummvm/scummvm/zip/ZipCoder.java
new file mode 100644
index 00000000000..d77548bcd05
--- /dev/null
+++ b/backends/platform/android/org/scummvm/scummvm/zip/ZipCoder.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (c) 2009, 2021, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package org.scummvm.scummvm.zip;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * Utility class for zipfile name and comment decoding and encoding
+ */
+class ZipCoder {
+
+    // Android-removed:
+    // private static final jdk.internal.access.JavaLangAccess JLA =
+    //    jdk.internal.access.SharedSecrets.getJavaLangAccess();
+
+    // Encoding/decoding is stateless, so make it singleton.
+    // Android-changed: use StandardCharsets.
+    // static final UTF8ZipCoder UTF8 = new UTF8ZipCoder(UTF_8.INSTANCE);
+    // ScummVM-changed: use ZipUtils.
+    static final UTF8ZipCoder UTF8 = new UTF8ZipCoder(ZipUtils.UTF_8);
+
+    public static ZipCoder get(Charset charset) {
+        // Android-changed: use equals method, not reference comparison.
+        // if (charset == UTF_8.INSTANCE) {
+        // ScummVM-changed: use ZipUtils.
+        if (ZipUtils.UTF_8.equals(charset)) {
+            return UTF8;
+        }
+        return new ZipCoder(charset);
+    }
+
+    String toString(byte[] ba, int off, int length) {
+        try {
+            return decoder().decode(ByteBuffer.wrap(ba, off, length)).toString();
+        } catch (CharacterCodingException x) {
+            throw new IllegalArgumentException(x);
+        }
+    }
+
+    String toString(byte[] ba, int length) {
+        return toString(ba, 0, length);
+    }
+
+    String toString(byte[] ba) {
+        return toString(ba, 0, ba.length);
+    }
+
+    byte[] getBytes(String s) {
+        try {
+            ByteBuffer bb = encoder().encode(CharBuffer.wrap(s));
+            int pos = bb.position();
+            int limit = bb.limit();
+            if (bb.hasArray() && pos == 0 && limit == bb.capacity()) {
+                return bb.array();
+            }
+            byte[] bytes = new byte[bb.limit() - bb.position()];
+            bb.get(bytes);
+            return bytes;
+        } catch (CharacterCodingException x) {
+            throw new IllegalArgumentException(x);
+        }
+    }
+
+    static String toStringUTF8(byte[] ba, int len) {
+        return UTF8.toString(ba, 0, len);
+    }
+
+    boolean isUTF8() {
+        return false;
+    }
+
+    // Hash code functions for ZipFile entry names. We generate the hash as-if
+    // we first decoded the byte sequence to a String, then appended '/' if no
+    // trailing slash was found, then called String.hashCode(). This
+    // normalization ensures we can simplify and speed up lookups.
+    //
+    // Does encoding error checking and hashing in a single pass for efficiency.
+    // On an error, this function will throw CharacterCodingException while the
+    // UTF8ZipCoder override will throw IllegalArgumentException, so we declare
+    // throws Exception to keep things simple.
+    int checkedHash(byte[] a, int off, int len) throws Exception {
+        if (len == 0) {
+            return 0;
+        }
+
+        int h = 0;
+        // cb will be a newly allocated CharBuffer with pos == 0,
+        // arrayOffset == 0, backed by an array.
+        CharBuffer cb = decoder().decode(ByteBuffer.wrap(a, off, len));
+        int limit = cb.limit();
+        char[] decoded = cb.array();
+        for (int i = 0; i < limit; i++) {
+            h = 31 * h + decoded[i];
+        }
+        if (limit > 0 && decoded[limit - 1] != '/') {
+            h = 31 * h + '/';
+        }
+        return h;
+    }
+
+    // Hash function equivalent of checkedHash for String inputs
+    static int hash(String name) {
+        int hsh = name.hashCode();
+        int len = name.length();
+        if (len > 0 && name.charAt(len - 1) != '/') {
+            hsh = hsh * 31 + '/';
+        }
+        return hsh;
+    }
+
+    boolean hasTrailingSlash(byte[] a, int end) {
+        byte[] slashBytes = slashBytes();
+        return end >= slashBytes.length &&
+            // ScummVM-changed: improve compatibility.
+            /*
+            Arrays.mismatch(a, end - slashBytes.length, end, slashBytes, 0, slashBytes.length) == -1;
+            */
+            Arrays.equals(Arrays.copyOfRange(a, end - slashBytes.length, end), slashBytes);
+    }
+
+    private byte[] slashBytes;
+    private final Charset cs;
+    protected CharsetDecoder dec;
+    private CharsetEncoder enc;
+
+    private ZipCoder(Charset cs) {
+        this.cs = cs;
+    }
+
+    protected CharsetDecoder decoder() {
+        if (dec == null) {
+            dec = cs.newDecoder()
+              .onMalformedInput(CodingErrorAction.REPORT)
+              .onUnmappableCharacter(CodingErrorAction.REPORT);
+        }
+        return dec;
+    }
+
+    private CharsetEncoder encoder() {
+        if (enc == null) {
+            enc = cs.newEncoder()
+              .onMalformedInput(CodingErrorAction.REPORT)
+              .onUnmappableCharacter(CodingErrorAction.REPORT);
+        }
+        return enc;
+    }
+
+    // This method produces an array with the bytes that will correspond to a
+    // trailing '/' in the chosen character encoding.
+    //
+    // While in most charsets a trailing slash will be encoded as the byte
+    // value of '/', this does not hold in the general case. E.g., in charsets
+    // such as UTF-16 and UTF-32 it will be represented by a sequence of 2 or 4
+    // bytes, respectively.
+    private byte[] slashBytes() {
+        if (slashBytes == null) {
+            // Take into account charsets that produce a BOM, e.g., UTF-16
+            byte[] slash = "/".getBytes(cs);
+            byte[] doubleSlash = "//".getBytes(cs);
+            slashBytes = Arrays.copyOfRange(doubleSlash, slash.length, doubleSlash.length);
+        }
+        return slashBytes;
+    }
+
+    static final class UTF8ZipCoder extends ZipCoder {
+
+        private UTF8ZipCoder(Charset utf8) {
+            super(utf8);
+        }
+
+        @Override
+        boolean isUTF8() {
+            return true;
+        }
+
+        @Override
+        String toString(byte[] ba, int off, int length) {
+            // Android-changed: JLA is not yet available.
+            // return JLA.newStringUTF8NoRepl(ba, off, length);
+            // ScummVM-changed: use ZipUtils.
+            return new String(ba, off, length, ZipUtils.UTF_8);
+        }
+
+        @Override
+        byte[] getBytes(String s) {
+            // Android-changed: JLA is not yet available.
+            // return JLA.getBytesUTF8NoRepl(s);
+            // ScummVM-changed: use ZipUtils.
+            return s.getBytes(ZipUtils.UTF_8);
+        }
+
+        @Override
+        int checkedHash(byte[] a, int off, int len) throws Exception {
+            if (len == 0) {
+                return 0;
+            }
+
+            int end = off + len;
+            int h = 0;
+            while (off < end) {
+                byte b = a[off];
+                if (b >= 0) {
+                    // ASCII, keep going
+                    h = 31 * h + b;
+                    off++;
+                } else {
+                    // Non-ASCII, fall back to decoding a String
+                    // We avoid using decoder() here since the UTF8ZipCoder is
+                    // shared and that decoder is not thread safe.
+                    // We use the JLA.newStringUTF8NoRepl variant to throw
+                    // exceptions eagerly when opening ZipFiles
+                    // Android-changed: JLA is not yet available.
+                    // return hash(JLA.newStringUTF8NoRepl(a, end - len, len));
+                    // ScummVM-changed: use ZipUtils.
+                    return hash(new String(a, end - len, len, ZipUtils.UTF_8));
+                }
+            }
+
+            if (a[end - 1] != '/') {
+                h = 31 * h + '/';
+            }
+            return h;
+        }
+
+        @Override
+        boolean hasTrailingSlash(byte[] a, int end) {
+            return end > 0 && a[end - 1] == '/';
+        }
+    }
+}
diff --git a/backends/platform/android/org/scummvm/scummvm/zip/ZipConstants.java b/backends/platform/android/org/scummvm/scummvm/zip/ZipConstants.java
new file mode 100644
index 00000000000..be86d0854c0
--- /dev/null
+++ b/backends/platform/android/org/scummvm/scummvm/zip/ZipConstants.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (c) 1995, 2020, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package org.scummvm.scummvm.zip;
+
+/*
+ * This interface defines the constants that are used by the classes
+ * which manipulate ZIP files.
+ *
+ * @author      David Connelly
+ * @since 1.1
+ */
+interface ZipConstants {
+
+    /**
+     * Local file (LOC) header signature.
+     */
+    static long LOCSIG = 0x04034b50L;   // "PK\003\004"
+
+    /**
+     * Extra local (EXT) header signature.
+     */
+    static long EXTSIG = 0x08074b50L;   // "PK\007\008"
+
+    /**
+     * Central directory (CEN) header signature.
+     */
+    static long CENSIG = 0x02014b50L;   // "PK\001\002"
+
+    /**
+     * End of central directory (END) header signature.
+     */
+    static long ENDSIG = 0x06054b50L;   // "PK\005\006"
+
+    /**
+     * Local file (LOC) header size in bytes (including signature).
+     */
+    static final int LOCHDR = 30;
+
+    /**
+     * Extra local (EXT) header size in bytes (including signature).
+     */
+    static final int EXTHDR = 16;
+
+    /**
+     * Central directory (CEN) header size in bytes (including signature).
+     */
+    static final int CENHDR = 46;
+
+    /**
+     * End of central directory (END) header size in bytes (including signature).
+     */
+    static final int ENDHDR = 22;
+
+    /**
+     * Local file (LOC) header version needed to extract field offset.
+     */
+    static final int LOCVER = 4;
+
+    /**
+     * Local file (LOC) header general purpose bit flag field offset.
+     */
+    static final int LOCFLG = 6;
+
+    /**
+     * Local file (LOC) header compression method field offset.
+     */
+    static final int LOCHOW = 8;
+
+    /**
+     * Local file (LOC) header modification time field offset.
+     */
+    static final int LOCTIM = 10;
+
+    /**
+     * Local file (LOC) header uncompressed file crc-32 value field offset.
+     */
+    static final int LOCCRC = 14;
+
+    /**
+     * Local file (LOC) header compressed size field offset.
+     */
+    static final int LOCSIZ = 18;
+
+    /**
+     * Local file (LOC) header uncompressed size field offset.
+     */
+    static final int LOCLEN = 22;
+
+    /**
+     * Local file (LOC) header filename length field offset.
+     */
+    static final int LOCNAM = 26;
+
+    /**
+     * Local file (LOC) header extra field length field offset.
+     */
+    static final int LOCEXT = 28;
+
+    /**
+     * Extra local (EXT) header uncompressed file crc-32 value field offset.
+     */
+    static final int EXTCRC = 4;
+
+    /**
+     * Extra local (EXT) header compressed size field offset.
+     */
+    static final int EXTSIZ = 8;
+
+    /**
+     * Extra local (EXT) header uncompressed size field offset.
+     */
+    static final int EXTLEN = 12;
+
+    /**
+     * Central directory (CEN) header version made by field offset.
+     */
+    static final int CENVEM = 4;
+
+    /**
+     * Central directory (CEN) header version needed to extract field offset.
+     */
+    static final int CENVER = 6;
+
+    /**
+     * Central directory (CEN) header encrypt, decrypt flags field offset.
+     */
+    static final int CENFLG = 8;
+
+    /**
+     * Central directory (CEN) header compression method field offset.
+     */
+    static final int CENHOW = 10;
+
+    /**
+     * Central directory (CEN) header modification time field offset.
+     */
+    static final int CENTIM = 12;
+
+    /**
+     * Central directory (CEN) header uncompressed file crc-32 value field offset.
+     */
+    static final int CENCRC = 16;
+
+    /**
+     * Central directory (CEN) header compressed size field offset.
+     */
+    static final int CENSIZ = 20;
+
+    /**
+     * Central directory (CEN) header uncompressed size field offset.
+     */
+    static final int CENLEN = 24;
+
+    /**
+     * Central directory (CEN) header filename length field offset.
+     */
+    static final int CENNAM = 28;
+
+    /**
+     * Central directory (CEN) header extra field length field offset.
+     */
+    static final int CENEXT = 30;
+
+    /**
+     * Central directory (CEN) header comment length field offset.
+     */
+    static final int CENCOM = 32;
+
+    /**
+     * Central directory (CEN) header disk number start field offset.
+     */
+    static final int CENDSK = 34;
+
+    /**
+     * Central directory (CEN) header internal file attributes field offset.
+     */
+    static final int CENATT = 36;
+
+    /**
+     * Central directory (CEN) header external file attributes field offset.
+     */
+    static final int CENATX = 38;
+
+    /**
+     * Central directory (CEN) header LOC header offset field offset.
+     */
+    static final int CENOFF = 42;
+
+    /**
+     * End of central directory (END) header number of entries on this disk field offset.
+     */
+    static final int ENDSUB = 8;
+
+    /**
+     * End of central directory (END) header total number of entries field offset.
+     */
+    static final int ENDTOT = 10;
+
+    /**
+     * End of central directory (END) header central directory size in bytes field offset.
+     */
+    static final int ENDSIZ = 12;
+
+    /**
+     * End of central directory (END) header offset for the first CEN header field offset.
+     */
+    static final int ENDOFF = 16;
+
+    /**
+     * End of central directory (END) header zip file comment length field offset.
+     */
+    static final int ENDCOM = 20;
+}
diff --git a/backends/platform/android/org/scummvm/scummvm/zip/ZipConstants64.java b/backends/platform/android/org/scummvm/scummvm/zip/ZipConstants64.java
new file mode 100644
index 00000000000..eddedabea36
--- /dev/null
+++ b/backends/platform/android/org/scummvm/scummvm/zip/ZipConstants64.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 1995, 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package org.scummvm.scummvm.zip;
+
+/*
+ * This class defines the constants that are used by the classes
+ * which manipulate Zip64 files.
+ */
+
+class ZipConstants64 {
+
+    /*
+     * ZIP64 constants
+     */
+    static final long ZIP64_ENDSIG = 0x06064b50L;  // "PK\006\006"
+    static final long ZIP64_LOCSIG = 0x07064b50L;  // "PK\006\007"
+    static final int  ZIP64_ENDHDR = 56;           // ZIP64 end header size
+    static final int  ZIP64_LOCHDR = 20;           // ZIP64 end loc header size
+    static final int  ZIP64_EXTHDR = 24;           // EXT header size
+    static final int  ZIP64_EXTID  = 0x0001;       // Extra field Zip64 header ID
+
+    static final int  ZIP64_MAGICCOUNT = 0xFFFF;
+    static final long ZIP64_MAGICVAL = 0xFFFFFFFFL;
+
+    /*
+     * Zip64 End of central directory (END) header field offsets
+     */
+    static final int  ZIP64_ENDLEN = 4;       // size of zip64 end of central dir
+    static final int  ZIP64_ENDVEM = 12;      // version made by
+    static final int  ZIP64_ENDVER = 14;      // version needed to extract
+    static final int  ZIP64_ENDNMD = 16;      // number of this disk
+    static final int  ZIP64_ENDDSK = 20;      // disk number of start
+    static final int  ZIP64_ENDTOD = 24;      // total number of entries on this disk
+    static final int  ZIP64_ENDTOT = 32;      // total number of entries
+    static final int  ZIP64_ENDSIZ = 40;      // central directory size in bytes
+    static final int  ZIP64_ENDOFF = 48;      // offset of first CEN header
+    static final int  ZIP64_ENDEXT = 56;      // zip64 extensible data sector
+
+    /*
+     * Zip64 End of central directory locator field offsets
+     */
+    static final int  ZIP64_LOCDSK = 4;       // disk number start
+    static final int  ZIP64_LOCOFF = 8;       // offset of zip64 end
+    static final int  ZIP64_LOCTOT = 16;      // total number of disks
+
+    /*
+     * Zip64 Extra local (EXT) header field offsets
+     */
+    static final int  ZIP64_EXTCRC = 4;       // uncompressed file crc-32 value
+    static final int  ZIP64_EXTSIZ = 8;       // compressed size, 8-byte
+    static final int  ZIP64_EXTLEN = 16;      // uncompressed size, 8-byte
+
+    /*
+     * Language encoding flag (general purpose flag bit 11)
+     *
+     * If this bit is set the filename and comment fields for this
+     * entry must be encoded using UTF-8.
+     */
+    static final int USE_UTF8 = 0x800;
+
+    /*
+     * Constants below are defined here (instead of in ZipConstants)
+     * to avoid being exposed as public fields of ZipFile, ZipEntry,
+     * ZipInputStream and ZipOutputstream.
+     */
+
+    /*
+     * Extra field header ID
+     */
+    static final int  EXTID_ZIP64 = 0x0001;    // Zip64
+    static final int  EXTID_NTFS  = 0x000a;    // NTFS
+    static final int  EXTID_UNIX  = 0x000d;    // UNIX
+    static final int  EXTID_EXTT  = 0x5455;    // Info-ZIP Extended Timestamp
+
+    /*
+     * EXTT timestamp flags
+     */
+    static final int  EXTT_FLAG_LMT = 0x1;       // LastModifiedTime
+    static final int  EXTT_FLAG_LAT = 0x2;       // LastAccessTime
+    static final int  EXTT_FLAT_CT  = 0x4;       // CreationTime
+
+    private ZipConstants64() {}
+}
diff --git a/backends/platform/android/org/scummvm/scummvm/zip/ZipEntry.java b/backends/platform/android/org/scummvm/scummvm/zip/ZipEntry.java
new file mode 100644
index 00000000000..3da25c4ce14
--- /dev/null
+++ b/backends/platform/android/org/scummvm/scummvm/zip/ZipEntry.java
@@ -0,0 +1,799 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (c) 1995, 2020, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package org.scummvm.scummvm.zip;
+
+import static org.scummvm.scummvm.zip.ZipUtils.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.attribute.FileTime;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+import java.time.ZoneId;
+
+import static org.scummvm.scummvm.zip.ZipConstants64.*;
+
+/**
+ * This class is used to represent a ZIP file entry.
+ *
+ * @author      David Connelly
+ * @since 1.1
+ */
+public class ZipEntry implements ZipConstants, Cloneable {
+
+    String name;        // entry name
+    long xdostime = -1; // last modification time (in extended DOS time,
+                        // where milliseconds lost in conversion might
+                        // be encoded into the upper half)
+    // ScummVM-changed: Don't use FileTime to improve compatibility.
+    /*
+    FileTime mtime;     // last modification time, from extra field data
+    FileTime atime;     // last access time, from extra field data
+    FileTime ctime;     // creation time, from extra field data
+    */
+    long crc = -1;      // crc-32 of entry data
+    long size = -1;     // uncompressed size of entry data
+    long csize = -1;    // compressed size of entry data
+    boolean csizeSet = false; // Only true if csize was explicitely set by
+                        // a call to setCompressedSize()
+    int method = -1;    // compression method
+    int flag = 0;       // general purpose flag
+    byte[] extra;       // optional extra field data for entry
+    String comment;     // optional comment string for entry
+    int extraAttributes = -1; // e.g. POSIX permissions, sym links.
+    // Android-added: Add dataOffset for internal use.
+    // Used by android.util.jar.StrictJarFile from frameworks.
+    long dataOffset;
+
+    /**
+     * Compression method for uncompressed entries.
+     */
+    public static final int STORED = 0;
+
+    /**
+     * Compression method for compressed (deflated) entries.
+     */
+    public static final int DEFLATED = 8;
+
+    /**
+     * DOS time constant for representing timestamps before 1980.
+     */
+    static final long DOSTIME_BEFORE_1980 = (1 << 21) | (1 << 16);
+
+    /**
+     * Approximately 128 years, in milliseconds (ignoring leap years etc).
+     *
+     * This establish an approximate high-bound value for DOS times in
+     * milliseconds since epoch, used to enable an efficient but
+     * sufficient bounds check to avoid generating extended last modified
+     * time entries.
+     *
+     * Calculating the exact number is locale dependent, would require loading
+     * TimeZone data eagerly, and would make little practical sense. Since DOS
+     * times theoretically go to 2107 - with compatibility not guaranteed
+     * after 2099 - setting this to a time that is before but near 2099
+     * should be sufficient.
+     * @hide
+     */
+    // Android-changed: Make UPPER_DOSTIME_BOUND public hidden for testing purposes.
+    public static final long UPPER_DOSTIME_BOUND =
+            128L * 365 * 24 * 60 * 60 * 1000;
+
+    // Android-added: New constructor for use by StrictJarFile native code.
+    /** @hide */
+    public ZipEntry(String name, String comment, long crc, long compressedSize,
+            long size, int compressionMethod, int xdostime, byte[] extra,
+            long dataOffset) {
+        this.name = name;
+        this.comment = comment;
+        this.crc = crc;
+        this.csize = compressedSize;
+        this.size = size;
+        this.method = compressionMethod;
+        this.xdostime = xdostime;
+        this.dataOffset = dataOffset;
+        this.setExtra0(extra, false, false);
+    }
+
+    /**
+     * Creates a new zip entry with the specified name.
+     *
+     * @param  name
+     *         The entry name
+     *
+     * @throws NullPointerException if the entry name is null
+     * @throws IllegalArgumentException if the entry name is longer than
+     *         0xFFFF bytes
+     */
+    public ZipEntry(String name) {
+        Objects.requireNonNull(name, "name");
+        // Android-changed: Explicitly use UTF_8 instead of the default charset.
+        // if (name.length() > 0xFFFF) {
+        //     throw new IllegalArgumentException("entry name too long");
+        // }
+        // ScummVM-changed: use ZipUtils.
+        if (name.getBytes(ZipUtils.UTF_8).length > 0xffff) {
+            throw new IllegalArgumentException(name + " too long: " +
+                    name.getBytes(ZipUtils.UTF_8).length);
+        }
+        this.name = name;
+    }
+
+    /**
+     * Creates a new zip entry with fields taken from the specified
+     * zip entry.
+     *
+     * @param  e
+     *         A zip Entry object
+     *
+     * @throws NullPointerException if the entry object is null
+     */
+    public ZipEntry(ZipEntry e) {
+        Objects.requireNonNull(e, "entry");
+        name = e.name;
+        xdostime = e.xdostime;
+        // ScummVM-changed: Don't use FileTime.
+        /*
+        mtime = e.mtime;
+        atime = e.atime;
+        ctime = e.ctime;
+        */
+        crc = e.crc;
+        size = e.size;
+        csize = e.csize;
+        csizeSet = e.csizeSet;
+        method = e.method;
+        flag = e.flag;
+        extra = e.extra;
+        comment = e.comment;
+        extraAttributes = e.extraAttributes;
+        // Android-added: Add dataOffset for internal use.
+        dataOffset = e.dataOffset;
+    }
+
+    /**
+     * Creates a new un-initialized zip entry
+     */
+    ZipEntry() {}
+
+    // BEGIN Android-added: Add dataOffset for internal use.
+    /** @hide */
+    public long getDataOffset() {
+        return dataOffset;
+    }
+    // END Android-added: Add dataOffset for internal use.
+
+    /**
+     * Returns the name of the entry.
+     * @return the name of the entry
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets the last modification time of the entry.
+     *
+     * <p> If the entry is output to a ZIP file or ZIP file formatted
+     * output stream the last modification time set by this method will
+     * be stored into the {@code date and time fields} of the zip file
+     * entry and encoded in standard {@code MS-DOS date and time format}.
+     * The {@link java.util.TimeZone#getDefault() default TimeZone} is
+     * used to convert the epoch time to the MS-DOS data and time.
+     *
+     * @param  time
+     *         The last modification time of the entry in milliseconds
+     *         since the epoch
+     *
+     * @see #getTime()
+     * @see #getLastModifiedTime()
+     */
+    // ScummVM-changed: Don't use FileTime.
+    /*
+    public void setTime(long time) {
+        this.xdostime = javaToExtendedDosTime(time);
+        // Avoid setting the mtime field if time is in the valid
+        // range for a DOS time
+        if (this.xdostime != DOSTIME_BEFORE_1980 && time <= UPPER_DOSTIME_BOUND) {
+            this.mtime = null;
+        } else {
+            int localYear = javaEpochToLocalDateTime(time).getYear();
+            if (localYear >= 1980 && localYear <= 2099) {
+                this.mtime = null;
+            } else {
+                this.mtime = FileTime.from(time, TimeUnit.MILLISECONDS);
+            }
+        }
+    }
+    */
+
+    /**
+     * Returns the last modification time of the entry.
+     *
+     * <p> If the entry is read from a ZIP file or ZIP file formatted
+     * input stream, this is the last modification time from the {@code
+     * date and time fields} of the zip file entry. The
+     * {@link java.util.TimeZone#getDefault() default TimeZone} is used
+     * to convert the standard MS-DOS formatted date and time to the
+     * epoch time.
+     *
+     * @return  The last modification time of the entry in milliseconds
+     *          since the epoch, or -1 if not specified
+     *
+     * //@see #setTime(long)
+     * //@see #setLastModifiedTime(FileTime)
+     */
+    public long getTime() {
+        // ScummVM-changed: Don't use FileTime.
+        /*
+        if (mtime != null) {
+            return mtime.toMillis();
+        }
+        */
+        return (xdostime != -1) ? extendedDosToJavaTime(xdostime) : -1;
+    }
+
+    /**
+     * Sets the last modification time of the entry in local date-time.
+     *
+     * <p> If the entry is output to a ZIP file or ZIP file formatted
+     * output stream the last modification time set by this method will
+     * be stored into the {@code date and time fields} of the zip file
+     * entry and encoded in standard {@code MS-DOS date and time format}.
+     * If the date-time set is out of the range of the standard {@code
+     * MS-DOS date and time format}, the time will also be stored into
+     * zip file entry's extended timestamp fields in {@code optional
+     * extra data} in UTC time. The {@link java.time.ZoneId#systemDefault()
+     * system default TimeZone} is used to convert the local date-time
+     * to UTC time.
+     *
+     * <p> {@code LocalDateTime} uses a precision of nanoseconds, whereas
+     * this class uses a precision of milliseconds. The conversion will
+     * truncate any excess precision information as though the amount in
+     * nanoseconds was subject to integer division by one million.
+     *
+     * @param  time
+     *         The last modification time of the entry in local date-time
+     *
+     * @see #getTimeLocal()
+     * @since 9
+     */
+    // ScummVM-changed: Don't use FileTime.
+    /*
+    public void setTimeLocal(LocalDateTime time) {
+        int year = time.getYear() - 1980;
+        if (year < 0) {
+            this.xdostime = DOSTIME_BEFORE_1980;
+        } else {
+            this.xdostime = ((year << 25 |
+                time.getMonthValue() << 21 |
+                time.getDayOfMonth() << 16 |
+                time.getHour() << 11 |
+                time.getMinute() << 5 |
+                time.getSecond() >> 1) & 0xffffffffL)
+                + ((long)(((time.getSecond() & 0x1) * 1000) +
+                      time.getNano() / 1000_000) << 32);
+        }
+        if (xdostime != DOSTIME_BEFORE_1980 && year <= 0x7f) {
+            this.mtime = null;
+        } else {
+            this.mtime = FileTime.from(
+                ZonedDateTime.of(time, ZoneId.systemDefault()).toInstant());
+        }
+    }
+    */
+
+    /**
+     * Returns the last modification time of the entry in local date-time.
+     *
+     * <p> If the entry is read from a ZIP file or ZIP file formatted
+     * input stream, this is the last modification time from the zip
+     * file entry's {@code optional extra data} if the extended timestamp
+     * fields are present. Otherwise, the last modification time is read
+     * from entry's standard MS-DOS formatted {@code date and time fields}.
+     *
+     * <p> The {@link java.time.ZoneId#systemDefault() system default TimeZone}
+     * is used to convert the UTC time to local date-time.
+     *
+     * @return  The last modification time of the entry in local date-time
+     *
+     * @see #setTimeLocal(LocalDateTime)
+     * @since 9
+     */
+    // ScummVM-changed: Don't use LocalDateTime.
+    /*
+    public LocalDateTime getTimeLocal() {
+        if (mtime != null) {
+            return LocalDateTime.ofInstant(mtime.toInstant(), ZoneId.systemDefault());
+        }
+        int ms = (int)(xdostime >> 32);
+        return LocalDateTime.of((int)(((xdostime >> 25) & 0x7f) + 1980),
+                             (int)((xdostime >> 21) & 0x0f),
+                             (int)((xdostime >> 16) & 0x1f),
+                             (int)((xdostime >> 11) & 0x1f),
+                             (int)((xdostime >> 5) & 0x3f),
+                             (int)((xdostime << 1) & 0x3e) + ms / 1000,
+                             (ms % 1000) * 1000_000);
+    }
+    */
+
+
+    /**
+     * Sets the last modification time of the entry.
+     *
+     * <p> When output to a ZIP file or ZIP file formatted output stream
+     * the last modification time set by this method will be stored into
+     * zip file entry's {@code date and time fields} in {@code standard
+     * MS-DOS date and time format}), and the extended timestamp fields
+     * in {@code optional extra data} in UTC time.
+     *
+     * @param  time
+     *         The last modification time of the entry
+     * @return This zip entry
+     *
+     * @throws NullPointerException if the {@code time} is null
+     *
+     * @see #getLastModifiedTime()
+     * @since 1.8
+     */
+    // ScummVM-changed: Don't use FileTime.
+    /*
+    public ZipEntry setLastModifiedTime(FileTime time) {
+        this.mtime = Objects.requireNonNull(time, "lastModifiedTime");
+        this.xdostime = javaToExtendedDosTime(time.to(TimeUnit.MILLISECONDS));
+        return this;
+    }
+    */
+
+    /**
+     * Returns the last modification time of the entry.
+     *
+     * <p> If the entry is read from a ZIP file or ZIP file formatted
+     * input stream, this is the last modification time from the zip
+     * file entry's {@code optional extra data} if the extended timestamp
+     * fields are present. Otherwise the last modification time is read
+     * from the entry's {@code date and time fields}, the {@link
+     * java.util.TimeZone#getDefault() default TimeZone} is used to convert
+     * the standard MS-DOS formatted date and time to the epoch time.
+     *
+     * @return The last modification time of the entry, null if not specified
+     *
+     * @see #setLastModifiedTime(FileTime)
+     * @since 1.8
+     */
+    // ScummVM-changed: Don't use FileTime.
+    /*
+    public FileTime getLastModifiedTime() {
+        if (mtime != null)
+            return mtime;
+        if (xdostime == -1)
+            return null;
+        return FileTime.from(getTime(), TimeUnit.MILLISECONDS);
+    }
+    */
+
+    /**
+     * Sets the last access time of the entry.
+     *
+     * <p> If set, the last access time will be stored into the extended
+     * timestamp fields of entry's {@code optional extra data}, when output
+     * to a ZIP file or ZIP file formatted stream.
+     *
+     * @param  time
+     *         The last access time of the entry
+     * @return This zip entry
+     *
+     * @throws NullPointerException if the {@code time} is null
+     *
+     * @see #getLastAccessTime()
+     * @since 1.8
+     */
+    // ScummVM-changed: Don't use FileTime.
+    /*
+    public ZipEntry setLastAccessTime(FileTime time) {
+        this.atime = Objects.requireNonNull(time, "lastAccessTime");
+        return this;
+    }
+    */
+
+    /**
+     * Returns the last access time of the entry.
+     *
+     * <p> The last access time is from the extended timestamp fields
+     * of entry's {@code optional extra data} when read from a ZIP file
+     * or ZIP file formatted stream.
+     *
+     * @return The last access time of the entry, null if not specified
+     * @see #setLastAccessTime(FileTime)
+     * @since 1.8
+     */
+    // ScummVM-changed: Don't use FileTime.
+    /*
+    public FileTime getLastAccessTime() {
+        return atime;
+    }
+    */
+
+    /**
+     * Sets the creation time of the entry.
+     *
+     * <p> If set, the creation time will be stored into the extended
+     * timestamp fields of entry's {@code optional extra data}, when
+     * output to a ZIP file or ZIP file formatted stream.
+     *
+     * @param  time
+     *         The creation time of the entry
+     * @return This zip entry
+     *
+     * @throws NullPointerException if the {@code time} is null
+     *
+     * @see #getCreationTime()
+     * @since 1.8
+     */
+    // ScummVM-changed: Don't use FileTime.
+    /*
+    public ZipEntry setCreationTime(FileTime time) {
+        this.ctime = Objects.requireNonNull(time, "creationTime");
+        return this;
+    }
+    */
+
+    /**
+     * Returns the creation time of the entry.
+     *
+     * <p> The creation time is from the extended timestamp fields of
+     * entry's {@code optional extra data} when read from a ZIP file
+     * or ZIP file formatted stream.
+     *
+     * @return the creation time of the entry, null if not specified
+     * @see #setCreationTime(FileTime)
+     * @since 1.8
+     */
+    // ScummVM-changed: Don't use FileTime.
+    /*
+    public FileTime getCreationTime() {
+        return ctime;
+    }
+    */
+
+    /**
+     * Sets the uncompressed size of the entry data.
+     *
+     * @param size the uncompressed size in bytes
+     *
+     * @throws IllegalArgumentException if the specified size is less
+     *         than 0, is greater than 0xFFFFFFFF when
+     *         <a href="package-summary.html#zip64">ZIP64 format</a> is not supported,
+     *         or is less than 0 when ZIP64 is supported
+     * @see #getSize()
+     */
+    public void setSize(long size) {
+        if (size < 0) {
+            throw new IllegalArgumentException("invalid entry size");
+        }
+        this.size = size;
+    }
+
+    /**
+     * Returns the uncompressed size of the entry data.
+     *
+     * @return the uncompressed size of the entry data, or -1 if not known
+     * @see #setSize(long)
+     */
+    public long getSize() {
+        return size;
+    }
+
+    /**
+     * Returns the size of the compressed entry data.
+     *
+     * <p> In the case of a stored entry, the compressed size will be the same
+     * as the uncompressed size of the entry.
+     *
+     * @return the size of the compressed entry data, or -1 if not known
+     * @see #setCompressedSize(long)
+     */
+    public long getCompressedSize() {
+        return csize;
+    }
+
+    /**
+     * Sets the size of the compressed entry data.
+     *
+     * @param csize the compressed size to set
+     *
+     * @see #getCompressedSize()
+     */
+    public void setCompressedSize(long csize) {
+        this.csize = csize;
+        this.csizeSet = true;
+    }
+
+    /**
+     * Sets the CRC-32 checksum of the uncompressed entry data.
+     *
+     * @param crc the CRC-32 value
+     *
+     * @throws IllegalArgumentException if the specified CRC-32 value is
+     *         less than 0 or greater than 0xFFFFFFFF
+     * @see #getCrc()
+     */
+    public void setCrc(long crc) {
+        if (crc < 0 || crc > 0xFFFFFFFFL) {
+            throw new IllegalArgumentException("invalid entry crc-32");
+        }
+        this.crc = crc;
+    }
+
+    /**
+     * Returns the CRC-32 checksum of the uncompressed entry data.
+     *
+     * @return the CRC-32 checksum of the uncompressed entry data, or -1 if
+     * not known
+     *
+     * @see #setCrc(long)
+     */
+    public long getCrc() {
+        return crc;
+    }
+
+    /**
+     * Sets the compression method for the entry.
+     *
+     * @param method the compression method, either STORED or DEFLATED
+     *
+     * @throws  IllegalArgumentException if the specified compression
+     *          method is invalid
+     * @see #getMethod()
+     */
+    public void setMethod(int method) {
+        if (method != STORED && method != DEFLATED) {
+            throw new IllegalArgumentException("invalid compression method");
+        }
+        this.method = method;
+    }
+
+    /**
+     * Returns the compression method of the entry.
+     *
+     * @return the compression method of the entry, or -1 if not specified
+     * @see #setMethod(int)
+     */
+    public int getMethod() {
+        return method;
+    }
+
+    /**
+     * Sets the optional extra field data for the entry.
+     *
+     * <p> Invoking this method may change this entry's last modification
+     * time, last access time and creation time, if the {@code extra} field
+     * data includes the extensible timestamp fields, such as {@code NTFS tag
+     * 0x0001} or {@code Info-ZIP Extended Timestamp}, as specified in
+     * <a href="http://www.info-zip.org/doc/appnote-19970311-iz.zip">Info-ZIP
+     * Application Note 970311</a>.
+     *
+     * @param  extra
+     *         The extra field data bytes
+     *
+     * @throws IllegalArgumentException if the length of the specified
+     *         extra field data is greater than 0xFFFF bytes
+     *
+     * @see #getExtra()
+     */
+    public void setExtra(byte[] extra) {
+        setExtra0(extra, false, true);
+    }
+
+    /**
+     * Sets the optional extra field data for the entry.
+     *
+     * @param extra
+     *        the extra field data bytes
+     * @param doZIP64
+     *        if true, set size and csize from ZIP64 fields if present
+     * @param isLOC
+     *        true if setting the extra field for a LOC, false if for
+     *        a CEN
+     */
+    void setExtra0(byte[] extra, boolean doZIP64, boolean isLOC) {
+        if (extra != null) {
+            if (extra.length > 0xFFFF) {
+                throw new IllegalArgumentException("invalid extra field length");
+            }
+            // extra fields are in "HeaderID(2)DataSize(2)Data... format
+            int off = 0;
+            int len = extra.length;
+            while (off + 4 < len) {
+                int tag = get16(extra, off);
+                int sz = get16(extra, off + 2);
+                off += 4;
+                if (off + sz > len)         // invalid data
+                    break;
+                switch (tag) {
+                case EXTID_ZIP64:
+                    if (doZIP64) {
+                        if (isLOC) {
+                            // LOC extra zip64 entry MUST include BOTH original
+                            // and compressed file size fields.
+                            // If invalid zip64 extra fields, simply skip. Even
+                            // it's rare, it's possible the entry size happens to
+                            // be the magic value and it "accidently" has some
+                            // bytes in extra match the id.
+                            if (sz >= 16) {
+                                size = get64(extra, off);
+                                csize = get64(extra, off + 8);
+                            }
+                        } else {
+                            // CEN extra zip64
+                            if (size == ZIP64_MAGICVAL) {
+                                if (off + 8 > len)  // invalid zip64 extra
+                                    break;          // fields, just skip
+                                size = get64(extra, off);
+                            }
+                            if (csize == ZIP64_MAGICVAL) {
+                                if (off + 16 > len)  // invalid zip64 extra
+                                    break;           // fields, just skip
+                                csize = get64(extra, off + 8);
+                            }
+                        }
+                    }
+                    break;
+                // ScummVM-changed: Don't use FileTime.
+                /*
+                case EXTID_NTFS:
+                    if (sz < 32) // reserved  4 bytes + tag 2 bytes + size 2 bytes
+                        break;   // m[a|c]time 24 bytes
+                    int pos = off + 4;               // reserved 4 bytes
+                    if (get16(extra, pos) !=  0x0001 || get16(extra, pos + 2) != 24)
+                        break;
+                    long wtime = get64(extra, pos + 4);
+                    if (wtime != WINDOWS_TIME_NOT_AVAILABLE) {
+                        mtime = winTimeToFileTime(wtime);
+                    }
+                    wtime = get64(extra, pos + 12);
+                    if (wtime != WINDOWS_TIME_NOT_AVAILABLE) {
+                        atime = winTimeToFileTime(wtime);
+                    }
+                    wtime = get64(extra, pos + 20);
+                    if (wtime != WINDOWS_TIME_NOT_AVAILABLE) {
+                        ctime = winTimeToFileTime(wtime);
+                    }
+                    break;
+                case EXTID_EXTT:
+                    int flag = Byte.toUnsignedInt(extra[off]);
+                    int sz0 = 1;
+                    // The CEN-header extra field contains the modification
+                    // time only, or no timestamp at all. 'sz' is used to
+                    // flag its presence or absence. But if mtime is present
+                    // in LOC it must be present in CEN as well.
+                    if ((flag & 0x1) != 0 && (sz0 + 4) <= sz) {
+                        mtime = unixTimeToFileTime(get32S(extra, off + sz0));
+                        sz0 += 4;
+                    }
+                    if ((flag & 0x2) != 0 && (sz0 + 4) <= sz) {
+                        atime = unixTimeToFileTime(get32S(extra, off + sz0));
+                        sz0 += 4;
+                    }
+                    if ((flag & 0x4) != 0 && (sz0 + 4) <= sz) {
+                        ctime = unixTimeToFileTime(get32S(extra, off + sz0));
+                        sz0 += 4;
+                    }
+                    break;
+                 */
+                 default:
+                }
+                off += sz;
+            }
+        }
+        this.extra = extra;
+    }
+
+    /**
+     * Returns the extra field data for the entry.
+     *
+     * @return the extra field data for the entry, or null if none
+     *
+     * @see #setExtra(byte[])
+     */
+    public byte[] getExtra() {
+        return extra;
+    }
+
+    /**
+     * Sets the optional comment string for the entry.
+     *
+     * <p>ZIP entry comments have maximum length of 0xffff. If the length of the
+     * specified comment string is greater than 0xFFFF bytes after encoding, only
+     * the first 0xFFFF bytes are output to the ZIP file entry.
+     *
+     * @param comment the comment string
+     *
+     * @see #getComment()
+     */
+    public void setComment(String comment) {
+        // BEGIN Android-added: Explicitly use UTF_8 instead of the default charset.
+        // ScummVM-changed: use ZipUtils.
+        if (comment != null && comment.getBytes(ZipUtils.UTF_8).length > 0xffff) {
+            throw new IllegalArgumentException(comment + " too long: " +
+                    comment.getBytes(ZipUtils.UTF_8).length);
+        }
+        // END Android-added: Explicitly use UTF_8 instead of the default charset.
+
+        this.comment = comment;
+    }
+
+    /**
+     * Returns the comment string for the entry.
+     *
+     * @return the comment string for the entry, or null if none
+     *
+     * @see #setComment(String)
+     */
+    public String getComment() {
+        return comment;
+    }
+
+    /**
+     * Returns true if this is a directory entry. A directory entry is
+     * defined to be one whose name ends with a '/'.
+     * @return true if this is a directory entry
+     */
+    public boolean isDirectory() {
+        return name.endsWith("/");
+    }
+
+    /**
+     * Returns a string representation of the ZIP entry.
+     */
+    public String toString() {
+        return getName();
+    }
+
+    /**
+     * Returns the hash code value for this entry.
+     */
+    public int hashCode() {
+        return name.hashCode();
+    }
+
+    /**
+     * Returns a copy of this entry.
+     */
+    public Object clone() {
+        try {
+            ZipEntry e = (ZipEntry)super.clone();
+            e.extra = (extra == null) ? null : extra.clone();
+            return e;
+        } catch (CloneNotSupportedException e) {
+            // This should never happen, since we are Cloneable
+            //throw new InternalError(e);
+            // ScummVM-changed: Don't use InternalError to improve compatibility.
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java b/backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java
new file mode 100644
index 00000000000..fb4c9a219ad
--- /dev/null
+++ b/backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java
@@ -0,0 +1,2123 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (c) 1995, 2021, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package org.scummvm.scummvm.zip;
+
+import java.io.Closeable;
+// ScummVM-changed: use FileInputStream.
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.RandomAccessFile;
+import java.io.UncheckedIOException;
+import java.lang.ref.Cleaner.Cleanable;
+// ScummVM-changed: use ByteBuffer
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.InvalidPathException;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.Files;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.TreeSet;
+import java.util.WeakHashMap;
+import java.util.function.Consumer;
+import java.util.function.IntFunction;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+// ScummVM-changed: import original classes.
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+import java.util.zip.ZipException;
+
+// ScummVM-changed: don't use internal APIs.
+/*
+import jdk.internal.access.SharedSecrets;
+import jdk.internal.misc.VM;
+import jdk.internal.ref.CleanerFactory;
+import jdk.internal.vm.annotation.Stable;
+import sun.security.util.SignatureFileVerifier;
+
+import dalvik.system.CloseGuard;
+import dalvik.system.ZipPathValidator;
+*/
+
+import static org.scummvm.scummvm.zip.ZipConstants64.*;
+import static org.scummvm.scummvm.zip.ZipUtils.*;
+
+/**
+ * This class is used to read entries from a zip file.
+ *
+ * <p> Unless otherwise noted, passing a {@code null} argument to a constructor
+ * or method in this class will cause a {@link NullPointerException} to be
+ * thrown.
+ *
+ * @apiNote
+ * To release resources used by this {@code ZipFile}, the {@link #close()} method
+ * should be called explicitly or by try-with-resources. Subclasses are responsible
+ * for the cleanup of resources acquired by the subclass. Subclasses that override
+ * {@link #finalize()} in order to perform cleanup should be modified to use alternative
+ * cleanup mechanisms such as {@link java.lang.ref.Cleaner} and remove the overriding
+ * {@code finalize} method.
+ *
+ * @author      David Connelly
+ * @since 1.1
+ */
+public class ZipFile implements ZipConstants, Closeable {
+
+    // ScummVM-changed: FileInputStream has no name.
+    //private final String name;     // zip file name
+    private volatile boolean closeRequested;
+
+    // The "resource" used by this zip file that needs to be
+    // cleaned after use.
+    // a) the input streams that need to be closed
+    // b) the list of cached Inflater objects
+    // c) the "native" source of this zip file.
+    // ScummVM-changed: don't use internal APIs.
+    private final /*@Stable*/ CleanableResource res;
+
+    /*
+    // Android-added: CloseGuard support.
+    private final CloseGuard guard = CloseGuard.get();
+    */
+
+    private static final int STORED = ZipEntry.STORED;
+    private static final int DEFLATED = ZipEntry.DEFLATED;
+
+    /**
+     * Mode flag to open a zip file for reading.
+     */
+    public static final int OPEN_READ = 0x1;
+
+    /**
+     * Mode flag to open a zip file and mark it for deletion.  The file will be
+     * deleted some time between the moment that it is opened and the moment
+     * that it is closed, but its contents will remain accessible via the
+     * {@code ZipFile} object until either the close method is invoked or the
+     * virtual machine exits.
+     */
+    public static final int OPEN_DELETE = 0x4;
+
+    // Android-changed: Additional ZipException throw scenario with ZipPathValidator.
+    /**
+     * Opens a zip file for reading.
+     *
+     * <p>First, if there is a security manager, its {@code checkRead}
+     * method is called with the {@code name} argument as its argument
+     * to ensure the read is allowed.
+     *
+     * <p>The UTF-8 {@link java.nio.charset.Charset charset} is used to
+     * decode the entry names and comments.
+     *
+     * <p>If the app targets Android U or above, zip file entry names containing
+     * ".." or starting with "/" passed here will throw a {@link java.util.zip.ZipException}.
+     * For more details, see {//@link dalvik.system.ZipPathValidator}.
+     *
+     * @param name the name of the zip file
+     * @throws java.util.zip.ZipException if (1) a ZIP format error has occurred or
+     *         (2) <code>targetSdkVersion >= BUILD.VERSION_CODES.UPSIDE_DOWN_CAKE</code>
+     *         and (the <code>name</code> argument contains ".." or starts with "/").
+     * @throws IOException if an I/O error has occurred
+     * @throws SecurityException if a security manager exists and its
+     *         {@code checkRead} method doesn't allow read access to the file.
+     *
+     * @see SecurityManager#checkRead(java.lang.String)
+     */
+    // ScummVM-changed: use FileInputStream.
+    /*
+    public ZipFile(String name) throws IOException {
+        this(new File(name), OPEN_READ);
+    }
+    */
+
+    /**
+     * Opens a new {@code ZipFile} to read from the specified
+     * {@code File} object in the specified mode.  The mode argument
+     * must be either {@code OPEN_READ} or {@code OPEN_READ | OPEN_DELETE}.
+     *
+     * <p>First, if there is a security manager, its {@code checkRead}
+     * method is called with the {@code name} argument as its argument to
+     * ensure the read is allowed.
+     *
+     * <p>The UTF-8 {@link java.nio.charset.Charset charset} is used to
+     * decode the entry names and comments
+     *
+     * @param file the ZIP file to be opened for reading
+     * @param mode the mode in which the file is to be opened
+     * @throws ZipException if a ZIP format error has occurred
+     * @throws IOException if an I/O error has occurred
+     * @throws SecurityException if a security manager exists and
+     *         its {@code checkRead} method
+     *         doesn't allow read access to the file,
+     *         or its {@code checkDelete} method doesn't allow deleting
+     *         the file when the {@code OPEN_DELETE} flag is set.
+     * @throws IllegalArgumentException if the {@code mode} argument is invalid
+     * @see SecurityManager#checkRead(java.lang.String)
+     * @since 1.3
+     */
+    // ScummVM-changed: use FileInputStream.
+    public ZipFile(/*File*/FileInputStream file, int mode) throws IOException {
+        // Android-changed: Use StandardCharsets.UTF_8.
+        // this(file, mode, UTF_8.INSTANCE);
+        // ScummVM-changed: use ZipUtils.
+        this(file, mode, ZipUtils.UTF_8);
+    }
+
+    /**
+     * Opens a ZIP file for reading given the specified File object.
+     *
+     * <p>The UTF-8 {@link java.nio.charset.Charset charset} is used to
+     * decode the entry names and comments.
+     *
+     * @param file the ZIP file to be opened for reading
+     * @throws ZipException if a ZIP format error has occurred
+     * @throws IOException if an I/O error has occurred
+     */
+    // ScummVM-changed: use FileInputStream.
+    public ZipFile(/*File*/FileInputStream file) throws ZipException, IOException {
+        this(file, OPEN_READ);
+    }
+
+    // Android-changed: Use of the hidden constructor with a new argument for zip path validation.
+    /**
+     * Opens a new {@code ZipFile} to read from the specified
+     * {@code File} object in the specified mode.  The mode argument
+     * must be either {@code OPEN_READ} or {@code OPEN_READ | OPEN_DELETE}.
+     *
+     * <p>First, if there is a security manager, its {@code checkRead}
+     * method is called with the {@code name} argument as its argument to
+     * ensure the read is allowed.
+     *
+     * @param file the ZIP file to be opened for reading
+     * @param mode the mode in which the file is to be opened
+     * @param charset
+     *        the {@linkplain java.nio.charset.Charset charset} to
+     *        be used to decode the ZIP entry name and comment that are not
+     *        encoded by using UTF-8 encoding (indicated by entry's general
+     *        purpose flag).
+     *
+     * @throws ZipException if a ZIP format error has occurred
+     * @throws IOException if an I/O error has occurred
+     *
+     * @throws SecurityException
+     *         if a security manager exists and its {@code checkRead}
+     *         method doesn't allow read access to the file,or its
+     *         {@code checkDelete} method doesn't allow deleting the
+     *         file when the {@code OPEN_DELETE} flag is set
+     *
+     * @throws IllegalArgumentException if the {@code mode} argument is invalid
+     *
+     * @see SecurityManager#checkRead(java.lang.String)
+     *
+     * @since 1.7
+     */
+    // ScummVM-changed: use FileInputStream.
+    public ZipFile(/*File*/FileInputStream file, int mode, Charset charset) throws IOException
+    {
+        this(file, mode, charset, /* enableZipPathValidator */ true);
+    }
+
+    // Android-added: New hidden constructor with an argument for zip path validation.
+    /** @hide */
+    // ScummVM-changed: use FileInputStream.
+    public ZipFile(/*File*/FileInputStream file, int mode, boolean enableZipPathValidator) throws IOException {
+        // ScummVM-changed: use ZipUtils.
+        this(file, mode, ZipUtils.UTF_8, enableZipPathValidator);
+    }
+
+    // Android-changed: Change existing constructor ZipFile(File file, int mode, Charset charset)
+    // to have a new argument enableZipPathValidator in order to set the isZipPathValidatorEnabled
+    // variable before calling the native method open().
+    /** @hide */
+    // ScummVM-changed: use FileInputStream.
+    public ZipFile(/*File*/FileInputStream file, int mode, Charset charset, boolean enableZipPathValidator)
+            throws IOException {
+        if (((mode & OPEN_READ) == 0) ||
+            ((mode & ~(OPEN_READ | OPEN_DELETE)) != 0)) {
+            throw new IllegalArgumentException("Illegal mode: 0x"+
+                                               Integer.toHexString(mode));
+        }
+        // ScummVM-changed: use FileInputStream.
+        /*
+        String name = file.getPath();
+        file = new File(name);
+        */
+        // Android-removed: SecurityManager is always null.
+        /*
+        @SuppressWarnings("removal")
+        SecurityManager sm = System.getSecurityManager();
+        if (sm != null) {
+            sm.checkRead(name);
+            if ((mode & OPEN_DELETE) != 0) {
+                sm.checkDelete(name);
+            }
+        }
+        */
+
+        Objects.requireNonNull(charset, "charset");
+
+        // ScummVM-changed: FileInputStream has no name.
+        /*
+        this.name = name;
+        */
+        // Android-removed: Skip perf counters.
+        // long t0 = System.nanoTime();
+
+        // Android-changed: pass isZipPathValidatorEnabled flag.
+        // this.res = new CleanableResource(this, ZipCoder.get(charset), file, mode);
+        // ScummVM-changed: don't use internal APIs.
+        /*
+        boolean isZipPathValidatorEnabled = enableZipPathValidator && !ZipPathValidator.isClear();
+        this.res = new CleanableResource(
+                this, ZipCoder.get(charset), file, mode, isZipPathValidatorEnabled);
+        */
+        this.res = new CleanableResource(this, ZipCoder.get(charset), file, mode);
+
+        // Android-removed: Skip perf counters.
+        // PerfCounter.getZipFileOpenTime().addElapsedTimeFrom(t0);
+        // PerfCounter.getZipFileCount().increment();
+    }
+
+    /**
+     * Opens a zip file for reading.
+     *
+     * <p>First, if there is a security manager, its {@code checkRead}
+     * method is called with the {@code name} argument as its argument
+     * to ensure the read is allowed.
+     *
+     * @param name the name of the zip file
+     * @param charset
+     *        the {@linkplain java.nio.charset.Charset charset} to
+     *        be used to decode the ZIP entry name and comment that are not
+     *        encoded by using UTF-8 encoding (indicated by entry's general
+     *        purpose flag).
+     *
+     * @throws ZipException if a ZIP format error has occurred
+     * @throws IOException if an I/O error has occurred
+     * @throws SecurityException
+     *         if a security manager exists and its {@code checkRead}
+     *         method doesn't allow read access to the file
+     *
+     * @see SecurityManager#checkRead(java.lang.String)
+     *
+     * @since 1.7
+     */
+    // ScummVM-changed: use FileInputStream.
+    /*
+    public ZipFile(String name, Charset charset) throws IOException
+    {
+        this(new File(name), OPEN_READ, charset);
+    }
+    */
+
+    /**
+     * Opens a ZIP file for reading given the specified File object.
+     *
+     * @param file the ZIP file to be opened for reading
+     * @param charset
+     *        The {@linkplain java.nio.charset.Charset charset} to be
+     *        used to decode the ZIP entry name and comment (ignored if
+     *        the <a href="package-summary.html#lang_encoding"> language
+     *        encoding bit</a> of the ZIP entry's general purpose bit
+     *        flag is set).
+     *
+     * @throws ZipException if a ZIP format error has occurred
+     * @throws IOException if an I/O error has occurred
+     *
+     * @since 1.7
+     */
+    // ScummVM-changed: use FileInputStream.
+    public ZipFile(/*File*/FileInputStream file, Charset charset) throws IOException
+    {
+        this(file, OPEN_READ, charset);
+    }
+
+    /**
+     * Returns the zip file comment, or null if none.
+     *
+     * @return the comment string for the zip file, or null if none
+     *
+     * @throws IllegalStateException if the zip file has been closed
+     *
+     * @since 1.7
+     */
+    public String getComment() {
+        synchronized (this) {
+            ensureOpen();
+            if (res.zsrc.comment == null) {
+                return null;
+            }
+            return res.zsrc.zc.toString(res.zsrc.comment);
+        }
+    }
+
+    /**
+     * Returns the zip file entry for the specified name, or null
+     * if not found.
+     *
+     * @param name the name of the entry
+     * @return the zip file entry, or null if not found
+     * @throws IllegalStateException if the zip file has been closed
+     */
+    public ZipEntry getEntry(String name) {
+        Objects.requireNonNull(name, "name");
+        ZipEntry entry = null;
+        synchronized (this) {
+            ensureOpen();
+            int pos = res.zsrc.getEntryPos(name, true);
+            if (pos != -1) {
+                entry = getZipEntry(name, pos);
+            }
+        }
+        return entry;
+    }
+
+    /**
+     * Returns an input stream for reading the contents of the specified
+     * zip file entry.
+     * <p>
+     * Closing this ZIP file will, in turn, close all input streams that
+     * have been returned by invocations of this method.
+     *
+     * @param entry the zip file entry
+     * @return the input stream for reading the contents of the specified
+     * zip file entry.
+     * @throws ZipException if a ZIP format error has occurred
+     * @throws IOException if an I/O error has occurred
+     * @throws IllegalStateException if the zip file has been closed
+     */
+    public InputStream getInputStream(ZipEntry entry) throws IOException {
+        Objects.requireNonNull(entry, "entry");
+        int pos;
+        ZipFileInputStream in;
+        Source zsrc = res.zsrc;
+        Set<InputStream> istreams = res.istreams;
+        synchronized (this) {
+            ensureOpen();
+            if (Objects.equals(lastEntryName, entry.name)) {
+                pos = lastEntryPos;
+            } else {
+                pos = zsrc.getEntryPos(entry.name, false);
+            }
+            if (pos == -1) {
+                return null;
+            }
+            in = new ZipFileInputStream(zsrc.cen, pos);
+            switch (CENHOW(zsrc.cen, pos)) {
+            case STORED:
+                synchronized (istreams) {
+                    istreams.add(in);
+                }
+                return in;
+            case DEFLATED:
+                // Inflater likes a bit of slack
+                // MORE: Compute good size for inflater stream:
+                long size = CENLEN(zsrc.cen, pos) + 2;
+                if (size > 65536) {
+                    // Android-changed: Use 64k buffer size, performs
+                    // better than 8k. See http://b/65491407.
+                    // size = 8192;
+                    size = 65536;
+                }
+                if (size <= 0) {
+                    size = 4096;
+                }
+                InputStream is = new ZipFileInflaterInputStream(in, res, (int)size);
+                synchronized (istreams) {
+                    istreams.add(is);
+                }
+                return is;
+            default:
+                throw new ZipException("invalid compression method");
+            }
+        }
+    }
+
+    // ScummVM-changed: don't use Cleanable to improve compatibility.
+    /*
+    private static class InflaterCleanupAction implements Runnable {
+        private final Inflater inf;
+        private final CleanableResource res;
+
+        InflaterCleanupAction(Inflater inf, CleanableResource res) {
+            this.inf = inf;
+            this.res = res;
+        }
+
+        @Override
+        public void run() {
+            res.releaseInflater(inf);
+        }
+    }
+    */
+
+    private class ZipFileInflaterInputStream extends InflaterInputStream {
+        private volatile boolean closeRequested;
+        private boolean eof = false;
+        // ScummVM-changed: don't use Cleanable to improve compatibility.
+        /*
+        private final Cleanable cleanable;
+        */
+        private final CleanableResource res;
+
+        ZipFileInflaterInputStream(ZipFileInputStream zfin,
+                                   CleanableResource res, int size) {
+            this(zfin, res, res.getInflater(), size);
+        }
+
+        private ZipFileInflaterInputStream(ZipFileInputStream zfin,
+                                           CleanableResource res,
+                                           Inflater inf, int size) {
+            /*
+            // Android-changed: ZipFileInflaterInputStream does not control its inflater's lifetime
+            // and hence it shouldn't be closed when the stream is closed.
+            // super(zfin, inf, size);
+            //super(zfin, inf, size, /* ownsInflater *\/ false);
+            this.cleanable = CleanerFactory.cleaner().register(this,
+                    new InflaterCleanupAction(inf, res));
+            */
+            // ScummVM-changed: don't use Cleanable to improve compatibility.
+            super(zfin, inf, size);
+            this.res = res;
+        }
+
+        public void close() throws IOException {
+            if (closeRequested)
+                return;
+            closeRequested = true;
+            super.close();
+            synchronized (res.istreams) {
+                res.istreams.remove(this);
+            }
+            // ScummVM-changed: don't use Cleanable to improve compatibility.
+            //cleanable.clean();
+            res.releaseInflater(inf);
+        }
+
+        // Override fill() method to provide an extra "dummy" byte
+        // at the end of the input stream. This is required when
+        // using the "nowrap" Inflater option.
+        protected void fill() throws IOException {
+            if (eof) {
+                throw new EOFException("Unexpected end of ZLIB input stream");
+            }
+            len = in.read(buf, 0, buf.length);
+            if (len == -1) {
+                buf[0] = 0;
+                len = 1;
+                eof = true;
+            }
+            inf.setInput(buf, 0, len);
+        }
+
+        public int available() throws IOException {
+            if (closeRequested)
+                return 0;
+            long avail = ((ZipFileInputStream)in).size() - inf.getBytesWritten();
+            return (avail > (long) Integer.MAX_VALUE ?
+                    Integer.MAX_VALUE : (int) avail);
+        }
+
+        // ScummVM-changed: don't use Cleanable to improve compatibility.
+        @SuppressWarnings("deprecation")
+        protected void finalize() throws Throwable {
+            close();
+        }
+    }
+
+    /**
+     * Returns the path name of the ZIP file.
+     * @return the path name of the ZIP file
+     */
+    // ScummVM-changed: FileInputStream has no name.
+    /*
+    public String getName() {
+        return name;
+    }
+    */
+
+    private class ZipEntryIterator<T extends ZipEntry>
+            implements Enumeration<T>, Iterator<T> {
+
+        private int i = 0;
+        private final int entryCount;
+
+        public ZipEntryIterator(int entryCount) {
+            this.entryCount = entryCount;
+        }
+
+        @Override
+        public boolean hasMoreElements() {
+            return hasNext();
+        }
+
+        @Override
+        public boolean hasNext() {
+            // Android-changed: check that file is open.
+            // return i < entryCount;
+            synchronized (ZipFile.this) {
+                ensureOpen();
+                return i < entryCount;
+            }
+        }
+
+        @Override
+        public T nextElement() {
+            return next();
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public T next() {
+            synchronized (ZipFile.this) {
+                ensureOpen();
+                if (!hasNext()) {
+                    throw new NoSuchElementException();
+                }
+                // each "entry" has 3 ints in table entries
+                return (T)getZipEntry(null, res.zsrc.getEntryPos(i++ * 3));
+            }
+        }
+
+        // ScummVM-changed: Improve compatibility
+        /*
+        @Override
+        public Iterator<T> asIterator() {
+            return this;
+        }
+        */
+    }
+
+    /**
+     * Returns an enumeration of the ZIP file entries.
+     * @return an enumeration of the ZIP file entries
+     * @throws IllegalStateException if the zip file has been closed
+     */
+    public Enumeration<? extends ZipEntry> entries() {
+        synchronized (this) {
+            ensureOpen();
+            return new ZipEntryIterator<ZipEntry>(res.zsrc.total);
+        }
+    }
+
+    // ScummVM-changed: Don't support JAR
+    /*
+    private Enumeration<JarEntry> jarEntries() {
+        synchronized (this) {
+            ensureOpen();
+            return new ZipEntryIterator<JarEntry>(res.zsrc.total);
+        }
+    }
+    */
+
+    // ScummVM-changed: Improve compatibility
+    /*
+    private class EntrySpliterator<T> extends Spliterators.AbstractSpliterator<T> {
+        private int index;
+        private final int fence;
+        private final IntFunction<T> gen;
+
+        EntrySpliterator(int index, int fence, IntFunction<T> gen) {
+            super((long)fence,
+                  Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.IMMUTABLE |
+                  Spliterator.NONNULL);
+            this.index = index;
+            this.fence = fence;
+            this.gen = gen;
+        }
+
+        @Override
+        public boolean tryAdvance(Consumer<? super T> action) {
+            if (action == null)
+                throw new NullPointerException();
+            if (index >= 0 && index < fence) {
+                synchronized (ZipFile.this) {
+                    ensureOpen();
+                    action.accept(gen.apply(res.zsrc.getEntryPos(index++ * 3)));
+                }
+                return true;
+            }
+            return false;
+        }
+    }
+    */
+
+    /**
+     * Returns an ordered {@code Stream} over the ZIP file entries.
+     *
+     * Entries appear in the {@code Stream} in the order they appear in
+     * the central directory of the ZIP file.
+     *
+     * @return an ordered {@code Stream} of entries in this ZIP file
+     * @throws IllegalStateException if the zip file has been closed
+     * @since 1.8
+     */
+    // ScummVM-changed: Improve compatibility
+    /*
+    public Stream<? extends ZipEntry> stream() {
+        synchronized (this) {
+            ensureOpen();
+            return StreamSupport.stream(new EntrySpliterator<>(0, res.zsrc.total,
+                pos -> getZipEntry(null, pos)), false);
+       }
+    }
+
+    private String getEntryName(int pos) {
+        byte[] cen = res.zsrc.cen;
+        int nlen = CENNAM(cen, pos);
+        ZipCoder zc = res.zsrc.zipCoderForPos(pos);
+        return zc.toString(cen, pos + CENHDR, nlen);
+    }
+    */
+
+    /*
+     * Returns an ordered {@code Stream} over the zip file entry names.
+     *
+     * Entry names appear in the {@code Stream} in the order they appear in
+     * the central directory of the ZIP file.
+     *
+     * @return an ordered {@code Stream} of entry names in this zip file
+     * @throws IllegalStateException if the zip file has been closed
+     * @since 10
+     */
+    // ScummVM-changed: Improve compatibility
+    /*
+    private Stream<String> entryNameStream() {
+        synchronized (this) {
+            ensureOpen();
+            return StreamSupport.stream(
+                new EntrySpliterator<>(0, res.zsrc.total, this::getEntryName), false);
+        }
+    }
+    */
+
+    /*
+     * Returns an ordered {@code Stream} over the zip file entries.
+     *
+     * Entries appear in the {@code Stream} in the order they appear in
+     * the central directory of the jar file.
+     *
+     * @return an ordered {@code Stream} of entries in this zip file
+     * @throws IllegalStateException if the zip file has been closed
+     * @since 10
+     */
+    // ScummVM-changed: Improve compatibility
+    /*
+    private Stream<JarEntry> jarStream() {
+        synchronized (this) {
+            ensureOpen();
+            return StreamSupport.stream(new EntrySpliterator<>(0, res.zsrc.total,
+                pos -> (JarEntry)getZipEntry(null, pos)), false);
+        }
+    }
+    */
+
+    private String lastEntryName;
+    private int lastEntryPos;
+
+    /* Check ensureOpen() before invoking this method */
+    private ZipEntry getZipEntry(String name, int pos) {
+        byte[] cen = res.zsrc.cen;
+        int nlen = CENNAM(cen, pos);
+        int elen = CENEXT(cen, pos);
+        int clen = CENCOM(cen, pos);
+
+        ZipCoder zc = res.zsrc.zipCoderForPos(pos);
+        if (name != null) {
+            // only need to check for mismatch of trailing slash
+            if (nlen > 0 &&
+                !name.isEmpty() &&
+                zc.hasTrailingSlash(cen, pos + CENHDR + nlen) &&
+                !name.endsWith("/"))
+            {
+                name += '/';
+            }
+        } else {
+            // invoked from iterator, use the entry name stored in cen
+            name = zc.toString(cen, pos + CENHDR, nlen);
+        }
+        ZipEntry e;
+        // ScummVM-changed: Don't support JAR
+        /*
+        if (this instanceof JarFile) {
+            // Android-changed: access method directly.
+            // e = Source.JUJA.entryFor((JarFile)this, name);
+            e = ((JarFile) this).entryFor(name);
+        } else {
+            e = new ZipEntry(name);
+        }
+        */
+        e = new ZipEntry(name);
+        e.flag = CENFLG(cen, pos);
+        e.xdostime = CENTIM(cen, pos);
+        e.crc = CENCRC(cen, pos);
+        e.size = CENLEN(cen, pos);
+        e.csize = CENSIZ(cen, pos);
+        e.method = CENHOW(cen, pos);
+        if (CENVEM_FA(cen, pos) == FILE_ATTRIBUTES_UNIX) {
+            // read all bits in this field, including sym link attributes
+            e.extraAttributes = CENATX_PERMS(cen, pos) & 0xFFFF;
+        }
+
+        if (elen != 0) {
+            int start = pos + CENHDR + nlen;
+            e.setExtra0(Arrays.copyOfRange(cen, start, start + elen), true, false);
+        }
+        if (clen != 0) {
+            int start = pos + CENHDR + nlen + elen;
+            e.comment = zc.toString(cen, start, clen);
+        }
+        lastEntryName = e.name;
+        lastEntryPos = pos;
+        return e;
+    }
+
+    /**
+     * Returns the number of entries in the ZIP file.
+     *
+     * @return the number of entries in the ZIP file
+     * @throws IllegalStateException if the zip file has been closed
+     */
+    public int size() {
+        synchronized (this) {
+            ensureOpen();
+            return res.zsrc.total;
+        }
+    }
+
+    private static class CleanableResource implements Runnable {
+        // The outstanding inputstreams that need to be closed
+        final Set<InputStream> istreams;
+
+        // List of cached Inflater objects for decompression
+        Deque<Inflater> inflaterCache;
+
+        // ScummVM-changed: don't use Cleanable to improve compatibility.
+        /*
+        final Cleanable cleanable;
+        */
+
+        Source zsrc;
+
+        // ScummVM-changed: use FileInputStream.
+        CleanableResource(ZipFile zf, ZipCoder zc, /*File*/FileInputStream file, int mode) throws IOException {
+            this(zf, zc, file, mode, false);
+        }
+
+        // Android-added: added extra enableZipPathValidator argument.
+        // ScummVM-changed: use FileInputStream.
+        CleanableResource(ZipFile zf, ZipCoder zc, /*File*/FileInputStream file,
+                int mode, boolean enableZipPathValidator) throws IOException {
+            // ScummVM-changed: don't use Cleanable to improve compatibility.
+            //this.cleanable = CleanerFactory.cleaner().register(zf, this);
+            this.istreams = Collections.newSetFromMap(new WeakHashMap<>());
+            this.inflaterCache = new ArrayDeque<>();
+            this.zsrc = Source.get(file, (mode & OPEN_DELETE) != 0, zc, enableZipPathValidator);
+        }
+
+        void clean() {
+            // ScummVM-changed: don't use Cleanable to improve compatibility, run clean ourselves.
+            /*
+            cleanable.clean();
+            */
+            run();
+        }
+
+        /*
+         * Gets an inflater from the list of available inflaters or allocates
+         * a new one.
+         */
+        Inflater getInflater() {
+            Inflater inf;
+            synchronized (inflaterCache) {
+                if ((inf = inflaterCache.poll()) != null) {
+                    return inf;
+                }
+            }
+            return new Inflater(true);
+        }
+
+        /*
+         * Releases the specified inflater to the list of available inflaters.
+         */
+        void releaseInflater(Inflater inf) {
+            Deque<Inflater> inflaters = this.inflaterCache;
+            if (inflaters != null) {
+                synchronized (inflaters) {
+                    // double checked!
+                    if (inflaters == this.inflaterCache) {
+                        inf.reset();
+                        inflaters.add(inf);
+                        return;
+                    }
+                }
+            }
+            // inflaters cache already closed - just end it.
+            inf.end();
+        }
+
+        public void run() {
+            IOException ioe = null;
+
+            // Release cached inflaters and close the cache first
+            Deque<Inflater> inflaters = this.inflaterCache;
+            if (inflaters != null) {
+                synchronized (inflaters) {
+                    // no need to double-check as only one thread gets a
+                    // chance to execute run() (Cleaner guarantee)...
+                    Inflater inf;
+                    while ((inf = inflaters.poll()) != null) {
+                        inf.end();
+                    }
+                    // close inflaters cache
+                    this.inflaterCache = null;
+                }
+            }
+
+            // Close streams, release their inflaters
+            if (istreams != null) {
+                synchronized (istreams) {
+                    if (!istreams.isEmpty()) {
+                        InputStream[] copy = istreams.toArray(new InputStream[0]);
+                        istreams.clear();
+                        for (InputStream is : copy) {
+                            try {
+                                is.close();
+                            } catch (IOException e) {
+                                if (ioe == null) ioe = e;
+                                else ioe.addSuppressed(e);
+                            }
+                        }
+                    }
+                }
+            }
+
+            // Release zip src
+            if (zsrc != null) {
+                synchronized (zsrc) {
+                    try {
+                        Source.release(zsrc);
+                        zsrc = null;
+                    } catch (IOException e) {
+                        if (ioe == null) ioe = e;
+                        else ioe.addSuppressed(e);
+                    }
+                }
+            }
+            if (ioe != null) {
+                // ScummVM-changed: use ZipUtils.
+                /*
+                throw new UncheckedIOException(ioe);
+                */
+                throw new ZipUtils.UncheckedIOException(ioe);
+            }
+        }
+    }
+
+    /**
+     * Closes the ZIP file.
+     *
+     * <p> Closing this ZIP file will close all of the input streams
+     * previously returned by invocations of the {@link #getInputStream
+     * getInputStream} method.
+     *
+     * @throws IOException if an I/O error has occurred
+     */
+    public void close() throws IOException {
+        if (closeRequested) {
+            return;
+        }
+        // Android-added: CloseGuard support.
+        // ScummVM-changed: don't use internal APIs.
+        /*
+        if (guard != null) {
+            guard.close();
+        }
+        */
+        closeRequested = true;
+
+        synchronized (this) {
+            // Close streams, release their inflaters, release cached inflaters
+            // and release zip source
+            try {
+                res.clean();
+            // ScummVM-changed: use ZipUtils.
+            } catch (ZipUtils.UncheckedIOException ioe) {
+                throw ioe.getCause();
+            }
+        }
+    }
+
+    private void ensureOpen() {
+        if (closeRequested) {
+            throw new IllegalStateException("zip file closed");
+        }
+        if (res.zsrc == null) {
+            throw new IllegalStateException("The object is not initialized.");
+        }
+    }
+
+    private void ensureOpenOrZipException() throws IOException {
+        if (closeRequested) {
+            throw new ZipException("ZipFile closed");
+        }
+    }
+
+    /*
+     * Inner class implementing the input stream used to read a
+     * (possibly compressed) zip file entry.
+     */
+    private class ZipFileInputStream extends InputStream {
+        private volatile boolean closeRequested;
+        private   long pos;     // current position within entry data
+        private   long startingPos; // Start position for the entry data
+        protected long rem;     // number of remaining bytes within entry
+        protected long size;    // uncompressed size of this entry
+
+        ZipFileInputStream(byte[] cen, int cenpos) {
+            rem = CENSIZ(cen, cenpos);
+            size = CENLEN(cen, cenpos);
+            pos = CENOFF(cen, cenpos);
+            // zip64
+            if (rem == ZIP64_MAGICVAL || size == ZIP64_MAGICVAL ||
+                pos == ZIP64_MAGICVAL) {
+                checkZIP64(cen, cenpos);
+            }
+            // negative for lazy initialization, see getDataOffset();
+            pos = - (pos + ZipFile.this.res.zsrc.locpos);
+        }
+
+        private void checkZIP64(byte[] cen, int cenpos) {
+            int off = cenpos + CENHDR + CENNAM(cen, cenpos);
+            int end = off + CENEXT(cen, cenpos);
+            while (off + 4 < end) {
+                int tag = get16(cen, off);
+                int sz = get16(cen, off + 2);
+                off += 4;
+                if (off + sz > end)         // invalid data
+                    break;
+                if (tag == EXTID_ZIP64) {
+                    if (size == ZIP64_MAGICVAL) {
+                        if (sz < 8 || (off + 8) > end)
+                            break;
+                        size = get64(cen, off);
+                        sz -= 8;
+                        off += 8;
+                    }
+                    if (rem == ZIP64_MAGICVAL) {
+                        if (sz < 8 || (off + 8) > end)
+                            break;
+                        rem = get64(cen, off);
+                        sz -= 8;
+                        off += 8;
+                    }
+                    if (pos == ZIP64_MAGICVAL) {
+                        if (sz < 8 || (off + 8) > end)
+                            break;
+                        pos = get64(cen, off);
+                        sz -= 8;
+                        off += 8;
+                    }
+                    break;
+                }
+                off += sz;
+            }
+        }
+
+        /*
+         * The Zip file spec explicitly allows the LOC extra data size to
+         * be different from the CEN extra data size. Since we cannot trust
+         * the CEN extra data size, we need to read the LOC to determine
+         * the entry data offset.
+         */
+        private long initDataOffset() throws IOException {
+            if (pos <= 0) {
+                byte[] loc = new byte[LOCHDR];
+                pos = -pos;
+                int len = ZipFile.this.res.zsrc.readFullyAt(loc, 0, loc.length, pos);
+                if (len != LOCHDR) {
+                    throw new ZipException("ZipFile error reading zip file");
+                }
+                if (LOCSIG(loc) != LOCSIG) {
+                    throw new ZipException("ZipFile invalid LOC header (bad signature)");
+                }
+                pos += LOCHDR + LOCNAM(loc) + LOCEXT(loc);
+                startingPos = pos; // Save starting position for the entry
+            }
+            return pos;
+        }
+
+        public int read(byte b[], int off, int len) throws IOException {
+            synchronized (ZipFile.this) {
+                ensureOpenOrZipException();
+                initDataOffset();
+                if (rem == 0) {
+                    return -1;
+                }
+                if (len > rem) {
+                    len = (int) rem;
+                }
+                if (len <= 0) {
+                    return 0;
+                }
+                len = ZipFile.this.res.zsrc.readAt(b, off, len, pos);
+                if (len > 0) {
+                    pos += len;
+                    rem -= len;
+                }
+            }
+            if (rem == 0) {
+                close();
+            }
+            return len;
+        }
+
+        public int read() throws IOException {
+            byte[] b = new byte[1];
+            if (read(b, 0, 1) == 1) {
+                return b[0] & 0xff;
+            } else {
+                return -1;
+            }
+        }
+
+        public long skip(long n) throws IOException {
+            synchronized (ZipFile.this) {
+                initDataOffset();
+                long newPos = pos + n;
+                if (n > 0) {
+                    // If we overflowed adding the skip value or are moving
+                    // past EOF, set the skip value to number of bytes remaining
+                    // to reach EOF
+                    if (newPos < 0 || n > rem) {
+                        n = rem;
+                    }
+                } else if (newPos < startingPos) {
+                    // Tried to position before BOF so set position to the
+                    // BOF and return the number of bytes we moved backwards
+                    // to reach BOF
+                    n = startingPos - pos;
+                }
+                pos += n;
+                rem -= n;
+            }
+            if (rem == 0) {
+                close();
+            }
+            return n;
+        }
+
+        public int available() {
+            return rem > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) rem;
+        }
+
+        public long size() {
+            return size;
+        }
+
+        public void close() {
+            if (closeRequested) {
+                return;
+            }
+            closeRequested = true;
+            rem = 0;
+            synchronized (res.istreams) {
+                res.istreams.remove(this);
+            }
+        }
+
+    }
+
+    /**
+     * Returns {@code true} if, and only if, the zip file begins with {@code
+     * LOCSIG}.
+     * @hide
+     */
+    // Android-added: Access startsWithLocHeader() directly.
+    // Make hidden public for use by sun.misc.URLClassPath
+    public boolean startsWithLocHeader() {
+        return res.zsrc.startsWithLoc;
+    }
+
+    // Android-changed: marked as protected so JarFile can access it.
+    /**
+     * Returns the names of the META-INF/MANIFEST.MF entry - if exists -
+     * and any signature-related files under META-INF. This method is used in
+     * JarFile, via SharedSecrets, as an optimization.
+     * @hide
+     */
+    // ScummVM-changed: Don't support JAR
+    /*
+    protected List<String> getManifestAndSignatureRelatedFiles() {
+        synchronized (this) {
+            ensureOpen();
+            Source zsrc = res.zsrc;
+            int[] metanames = zsrc.signatureMetaNames;
+            List<String> files = null;
+            if (zsrc.manifestPos >= 0) {
+                files = new ArrayList<>();
+                files.add(getEntryName(zsrc.manifestPos));
+            }
+            if (metanames != null) {
+                if (files == null) {
+                    files = new ArrayList<>();
+                }
+                for (int i = 0; i < metanames.length; i++) {
+                    files.add(getEntryName(metanames[i]));
+                }
+            }
+            return files == null ? List.of() : files;
+        }
+    }
+    */
+
+    /**
+     * Returns the number of the META-INF/MANIFEST.MF entries, case insensitive.
+     * When this number is greater than 1, JarVerifier will treat a file as
+     * unsigned.
+     */
+    // ScummVM-changed: Don't support JAR
+    /*
+    private int getManifestNum() {
+        synchronized (this) {
+            ensureOpen();
+            return res.zsrc.manifestNum;
+        }
+    }
+    */
+
+    // Android-changed: marked public and @hide as alternative to JavaUtilZipFileAccess.getManifestName.
+    /**
+     * Returns the name of the META-INF/MANIFEST.MF entry, ignoring
+     * case. If {@code onlyIfSignatureRelatedFiles} is true, we only return the
+     * manifest if there is also at least one signature-related file.
+     * This method is used in JarFile, via SharedSecrets, as an optimization
+     * when looking up the manifest file.
+     * @hide
+     */
+    // ScummVM-changed: Don't support JAR
+    /*
+    protected String getManifestName(boolean onlyIfSignatureRelatedFiles) {
+        synchronized (this) {
+            ensureOpen();
+            Source zsrc = res.zsrc;
+            int pos = zsrc.manifestPos;
+            if (pos >= 0 && (!onlyIfSignatureRelatedFiles || zsrc.signatureMetaNames != null)) {
+                return getEntryName(pos);
+            }
+        }
+        return null;
+    }
+    */
+
+    /**
+     * Returns the versions for which there exists a non-directory
+     * entry that begin with "META-INF/versions/" (case ignored).
+     * This method is used in JarFile, via SharedSecrets, as an
+     * optimization when looking up potentially versioned entries.
+     * Returns an empty array if no versioned entries exist.
+     */
+    // ScummVM-changed: Don't support JAR
+    /*
+    private int[] getMetaInfVersions() {
+        synchronized (this) {
+            ensureOpen();
+            return res.zsrc.metaVersions;
+        }
+    }
+    */
+
+    // Android-removed: this code does not run on Windows and JavaUtilZipFileAccess is not imported.
+    /*
+    private static boolean isWindows;
+
+    static {
+        SharedSecrets.setJavaUtilZipFileAccess(
+            new JavaUtilZipFileAccess() {
+                @Override
+                public boolean startsWithLocHeader(ZipFile zip) {
+                    return zip.res.zsrc.startsWithLoc;
+                }
+                @Override
+                public List<String> getManifestAndSignatureRelatedFiles(JarFile jar) {
+                    return ((ZipFile)jar).getManifestAndSignatureRelatedFiles();
+                }
+                @Override
+                public int getManifestNum(JarFile jar) {
+                    return ((ZipFile)jar).getManifestNum();
+                }
+                @Override
+                public String getManifestName(JarFile jar, boolean onlyIfHasSignatureRelatedFiles) {
+                    return ((ZipFile)jar).getManifestName(onlyIfHasSignatureRelatedFiles);
+                }
+                @Override
+                public int[] getMetaInfVersions(JarFile jar) {
+                    return ((ZipFile)jar).getMetaInfVersions();
+                }
+                @Override
+                public Enumeration<JarEntry> entries(ZipFile zip) {
+                    return zip.jarEntries();
+                }
+                @Override
+                public Stream<JarEntry> stream(ZipFile zip) {
+                    return zip.jarStream();
+                }
+                @Override
+                public Stream<String> entryNameStream(ZipFile zip) {
+                    return zip.entryNameStream();
+                }
+                @Override
+                public int getExtraAttributes(ZipEntry ze) {
+                    return ze.extraAttributes;
+                }
+                @Override
+                public void setExtraAttributes(ZipEntry ze, int extraAttrs) {
+                    ze.extraAttributes = extraAttrs;
+                }
+
+             }
+        );
+        isWindows = VM.getSavedProperty("os.name").contains("Windows");
+    }
+    */
+
+    private static class Source {
+        // While this is only used from ZipFile, defining it there would cause
+        // a bootstrap cycle that would leave this initialized as null
+        // Android-removed: JavaUtilJarAccess is not available.
+        // private static final JavaUtilJarAccess JUJA = SharedSecrets.javaUtilJarAccess();
+        // "META-INF/".length()
+        private static final int META_INF_LEN = 9;
+        private static final int[] EMPTY_META_VERSIONS = new int[0];
+
+        private final Key key;               // the key in files
+        // ScummVM-changed: don't use internal APIs.
+        private final /*@Stable*/ ZipCoder zc;   // zip coder used to decode/encode
+
+        private int refs = 1;
+
+        // ScummVM-changed: use FileInputStream.
+        /*
+        private RandomAccessFile zfile;      // zfile of the underlying zip file
+        */
+        private FileInputStream zfile;
+        private byte[] cen;                  // CEN & ENDHDR
+        private long locpos;                 // position of first LOC header (usually 0)
+        private byte[] comment;              // zip file comment
+        // ScummVM-changed: Don't support JAR
+        /*
+                                             // list of meta entries in META-INF dir
+        private int   manifestPos = -1;      // position of the META-INF/MANIFEST.MF, if exists
+        private int   manifestNum = 0;       // number of META-INF/MANIFEST.MF, case insensitive
+        private int[] signatureMetaNames;    // positions of signature related entries, if such exist
+        private int[] metaVersions;          // list of unique versions found in META-INF/versions/
+        */
+        private final boolean startsWithLoc; // true, if zip file starts with LOCSIG (usually true)
+
+        // A Hashmap for all entries.
+        //
+        // A cen entry of Zip/JAR file. As we have one for every entry in every active Zip/JAR,
+        // We might have a lot of these in a typical system. In order to save space we don't
+        // keep the name in memory, but merely remember a 32 bit {@code hash} value of the
+        // entry name and its offset {@code pos} in the central directory hdeader.
+        //
+        // private static class Entry {
+        //     int hash;       // 32 bit hashcode on name
+        //     int next;       // hash chain: index into entries
+        //     int pos;        // Offset of central directory file header
+        // }
+        // private Entry[] entries;             // array of hashed cen entry
+        //
+        // To reduce the total size of entries further, we use a int[] here to store 3 "int"
+        // {@code hash}, {@code next} and {@code pos} for each entry. The entry can then be
+        // referred by their index of their positions in the {@code entries}.
+        //
+        private int[] entries;                  // array of hashed cen entry
+
+        // Checks the entry at offset pos in the CEN, calculates the Entry values as per above,
+        // then returns the length of the entry name.
+        private int checkAndAddEntry(int pos, int index)
+            throws ZipException
+        {
+            byte[] cen = this.cen;
+            if (CENSIG(cen, pos) != CENSIG) {
+                zerror("invalid CEN header (bad signature)");
+            }
+            int method = CENHOW(cen, pos);
+            int flag   = CENFLG(cen, pos);
+            if ((flag & 1) != 0) {
+                zerror("invalid CEN header (encrypted entry)");
+            }
+            if (method != STORED && method != DEFLATED) {
+                zerror("invalid CEN header (bad compression method: " + method + ")");
+            }
+            int entryPos = pos + CENHDR;
+            int nlen = CENNAM(cen, pos);
+            if (entryPos + nlen > cen.length - ENDHDR) {
+                zerror("invalid CEN header (bad header size)");
+            }
+            try {
+                ZipCoder zcp = zipCoderForPos(pos);
+                int hash = zcp.checkedHash(cen, entryPos, nlen);
+                int hsh = (hash & 0x7fffffff) % tablelen;
+                int next = table[hsh];
+                table[hsh] = index;
+                // Record the CEN offset and the name hash in our hash cell.
+                entries[index++] = hash;
+                entries[index++] = next;
+                entries[index  ] = pos;
+            } catch (Exception e) {
+                zerror("invalid CEN header (bad entry name)");
+            }
+            return nlen;
+        }
+
+        private int getEntryHash(int index) { return entries[index]; }
+        private int getEntryNext(int index) { return entries[index + 1]; }
+        private int getEntryPos(int index)  { return entries[index + 2]; }
+        private static final int ZIP_ENDCHAIN  = -1;
+        private int total;                   // total number of entries
+        private int[] table;                 // Hash chain heads: indexes into entries
+        private int tablelen;                // number of hash heads
+
+        // Android-changed: isZipFilePathValidatorEnabled added as Key part. Key is used as key in
+        // files HashMap, so not including it could lead to opening ZipFile w/o entry names
+        // validation.
+        private static class Key {
+            // BEGIN ScummVM-changed: use FileInputStream.
+            /*
+            final BasicFileAttributes attrs;
+            File file;
+            final boolean utf8;
+            // Android-added: isZipFilePathValidatorEnabled added as Key part.
+            final boolean isZipFilePathValidatorEnabled;
+
+            public Key(File file, BasicFileAttributes attrs, ZipCoder zc) {
+                this(file, attrs, zc, /* isZipFilePathValidatorEnabled= *\/ false);
+            }
+
+            // Android-added: added constructor with isZipFilePathValidatorEnabled argument.
+            public Key(File file, BasicFileAttributes attrs, ZipCoder zc,
+                    boolean isZipFilePathValidatorEnabled) {
+                this.attrs = attrs;
+                this.file = file;
+                this.utf8 = zc.isUTF8();
+                this.isZipFilePathValidatorEnabled = isZipFilePathValidatorEnabled;
+            }
+
+            public int hashCode() {
+                long t = utf8 ? 0 : Long.MAX_VALUE;
+                t += attrs.lastModifiedTime().toMillis();
+                // Android-changed: include izZipFilePathValidatorEnabled in hash computation.
+                // return ((int)(t ^ (t >>> 32))) + file.hashCode();
+                return ((int)(t ^ (t >>> 32))) + file.hashCode()
+                        + Boolean.hashCode(isZipFilePathValidatorEnabled);
+            }
+
+            public boolean equals(Object obj) {
+                if (obj instanceof Key key) {
+                    if (key.utf8 != utf8) {
+                        return false;
+                    }
+                    if (!attrs.lastModifiedTime().equals(key.attrs.lastModifiedTime())) {
+                        return false;
+                    }
+                    // Android-added: include isZipFilePathValidatorEnabled as equality part.
+                    if (key.isZipFilePathValidatorEnabled != isZipFilePathValidatorEnabled) {
+                        return false;
+                    }
+                    Object fk = attrs.fileKey();
+                    if (fk != null) {
+                        return fk.equals(key.attrs.fileKey());
+                    } else {
+                        return file.equals(key.file);
+                    }
+                }
+                return false;
+            }
+            */
+
+            final FileInputStream fis;
+
+            public Key(FileInputStream fis) {
+                this.fis = fis;
+            }
+
+            public int hashCode() {
+                return fis.hashCode();
+            }
+
+            public boolean equals(Object obj) {
+                if (!(obj instanceof Key)) {
+                    return false;
+                }
+
+                Key okey = (Key)obj;
+                return fis.equals(okey.fis);
+            }
+            // END ScummVM-changed: use FileInputStream.
+        }
+        private static final HashMap<Key, Source> files = new HashMap<>();
+
+
+        // Android-changed: pass izZipFilePathValidatorEnabled argument.
+        // static Source get(File file, boolean toDelete, ZipCoder zc) throws IOException {
+        // ScummVM-changed: use FileInputStream.
+        static Source get(/*File*/FileInputStream file, boolean toDelete, ZipCoder zc,
+                boolean isZipPathValidatorEnabled) throws IOException {
+            final Key key;
+            // ScummVM-changed: use FileInputStream and don't use internal APIs.
+            /*
+            try {
+                // BEGIN Android-changed: isZipFilePathValidatorEnabled passed as part of Key.
+                /*
+                key = new Key(file,
+                        Files.readAttributes(file.toPath(), BasicFileAttributes.class),
+                        zc);
+                *\/
+                key = new Key(file,
+                        Files.readAttributes(file.toPath(), BasicFileAttributes.class),
+                        zc, isZipPathValidatorEnabled);
+                // END Android-changed: isZipFilePathValidatorEnabled passed as part of Key.
+            } catch (InvalidPathException ipe) {
+                throw new IOException(ipe);
+            }
+            */
+            key = new Key(file);
+            Source src;
+            synchronized (files) {
+                src = files.get(key);
+                if (src != null) {
+                    src.refs++;
+                    return src;
+                }
+            }
+            src = new Source(key, toDelete, zc);
+
+            synchronized (files) {
+                if (files.containsKey(key)) {    // someone else put in first
+                    src.close();                 // close the newly created one
+                    src = files.get(key);
+                    src.refs++;
+                    return src;
+                }
+                files.put(key, src);
+                return src;
+            }
+        }
+
+        static void release(Source src) throws IOException {
+            synchronized (files) {
+                if (src != null && --src.refs == 0) {
+                    files.remove(src.key);
+                    src.close();
+                }
+            }
+        }
+
+        private Source(Key key, boolean toDelete, ZipCoder zc) throws IOException {
+            this.zc = zc;
+            this.key = key;
+            // ScummVM-changed: use FileInputStream.
+            /*
+            if (toDelete) {
+                // BEGIN Android-changed: we are not targeting Windows, keep else branch only. Also
+                // open file with O_CLOEXEC flag set.
+                /*
+                if (isWindows) {
+                    this.zfile = SharedSecrets.getJavaIORandomAccessFileAccess()
+                                              .openAndDelete(key.file, "r");
+                } else {
+                    this.zfile = new RandomAccessFile(key.file, "r");
+                    key.file.delete();
+                }
+                *\/
+                this.zfile = new RandomAccessFile(key.file, "r", /* setCloExecFlag= *\/ true);
+                key.file.delete();
+                // END Android-changed: we are not targeting Windows, keep else branch only.
+            } else {
+                // Android-changed: open with O_CLOEXEC flag set.
+                // this.zfile = new RandomAccessFile(key.file, "r");
+                this.zfile = new RandomAccessFile(key.file, "r", /* setCloExecFlag= *\/ true);
+            }
+            */
+            this.zfile = key.fis;
+            try {
+                initCEN(-1);
+                byte[] buf = new byte[4];
+                readFullyAt(buf, 0, 4, 0);
+                this.startsWithLoc = (LOCSIG(buf) == LOCSIG);
+            } catch (IOException x) {
+                try {
+                    this.zfile.close();
+                } catch (IOException xx) {}
+                throw x;
+            }
+        }
+
+        private void close() throws IOException {
+            zfile.close();
+            zfile = null;
+            cen = null;
+            entries = null;
+            table = null;
+            // ScummVM-changed: Don't support JAR
+            /*
+            manifestPos = -1;
+            manifestNum = 0;
+            signatureMetaNames = null;
+            metaVersions = EMPTY_META_VERSIONS;
+            */
+        }
+
+        private static final int BUF_SIZE = 8192;
+        private final int readFullyAt(byte[] buf, int off, int len, long pos)
+            throws IOException
+        {
+            synchronized (zfile) {
+                // ScummVM-changed: Don't seek in FileInputStream to allow simultaneous use of it.
+                /*
+                zfile.seek(pos);
+                int N = len;
+                while (N > 0) {
+                    int n = Math.min(BUF_SIZE, N);
+                    zfile.readFully(buf, off, n);
+                    off += n;
+                    N -= n;
+                }
+                */
+                ByteBuffer buffer = ByteBuffer.wrap(buf, off, len);
+                while(buffer.remaining() > 0) {
+                    int read = zfile.getChannel().read(buffer, pos);
+                    pos += read;
+                }
+                return len;
+            }
+        }
+
+        private final int readAt(byte[] buf, int off, int len, long pos)
+            throws IOException
+        {
+            synchronized (zfile) {
+                // ScummVM-changed: Don't seek in FileInputStream to allow simultaneous use of it.
+                /*
+                zfile.seek(pos);
+                return zfile.read(buf, off, len);
+                */
+                ByteBuffer buffer = ByteBuffer.wrap(buf, off, len);
+                return zfile.getChannel().read(buffer, pos);
+            }
+        }
+
+
+        private static class End {
+            int  centot;     // 4 bytes
+            long cenlen;     // 4 bytes
+            long cenoff;     // 4 bytes
+            long endpos;     // 4 bytes
+        }
+
+        /*
+         * Searches for end of central directory (END) header. The contents of
+         * the END header will be read and placed in endbuf. Returns the file
+         * position of the END header, otherwise returns -1 if the END header
+         * was not found or an error occurred.
+         */
+        private End findEND() throws IOException {
+            // ScummVM-changed: use FileInputStream.
+            /*
+            long ziplen = zfile.length();
+            */
+            long ziplen = zfile.getChannel().size();
+            if (ziplen <= 0)
+                zerror("zip file is empty");
+            End end = new End();
+            byte[] buf = new byte[READBLOCKSZ];
+            long minHDR = (ziplen - END_MAXLEN) > 0 ? ziplen - END_MAXLEN : 0;
+            long minPos = minHDR - (buf.length - ENDHDR);
+            for (long pos = ziplen - buf.length; pos >= minPos; pos -= (buf.length - ENDHDR)) {
+                int off = 0;
+                if (pos < 0) {
+                    // Pretend there are some NUL bytes before start of file
+                    off = (int)-pos;
+                    Arrays.fill(buf, 0, off, (byte)0);
+                }
+                int len = buf.length - off;
+                if (readFullyAt(buf, off, len, pos + off) != len ) {
+                    zerror("zip END header not found");
+                }
+                // Now scan the block backwards for END header signature
+                for (int i = buf.length - ENDHDR; i >= 0; i--) {
+                    if (buf[i+0] == (byte)'P'    &&
+                        buf[i+1] == (byte)'K'    &&
+                        buf[i+2] == (byte)'\005' &&
+                        buf[i+3] == (byte)'\006') {
+                        // Found ENDSIG header
+                        byte[] endbuf = Arrays.copyOfRange(buf, i, i + ENDHDR);
+                        end.centot = ENDTOT(endbuf);
+                        end.cenlen = ENDSIZ(endbuf);
+                        end.cenoff = ENDOFF(endbuf);
+                        end.endpos = pos + i;
+                        int comlen = ENDCOM(endbuf);
+                        if (end.endpos + ENDHDR + comlen != ziplen) {
+                            // ENDSIG matched, however the size of file comment in it does
+                            // not match the real size. One "common" cause for this problem
+                            // is some "extra" bytes are padded at the end of the zipfile.
+                            // Let's do some extra verification, we don't care about the
+                            // performance in this situation.
+                            byte[] sbuf = new byte[4];
+                            long cenpos = end.endpos - end.cenlen;
+                            long locpos = cenpos - end.cenoff;
+                            if  (cenpos < 0 ||
+                                 locpos < 0 ||
+                                 readFullyAt(sbuf, 0, sbuf.length, cenpos) != 4 ||
+                                 GETSIG(sbuf) != CENSIG ||
+                                 readFullyAt(sbuf, 0, sbuf.length, locpos) != 4 ||
+                                 GETSIG(sbuf) != LOCSIG) {
+                                continue;
+                            }
+                        }
+                        if (comlen > 0) {    // this zip file has comlen
+                            comment = new byte[comlen];
+                            if (readFullyAt(comment, 0, comlen, end.endpos + ENDHDR) != comlen) {
+                                zerror("zip comment read failed");
+                            }
+                        }
+                        // must check for a zip64 end record; it is always permitted to be present
+                        try {
+                            byte[] loc64 = new byte[ZIP64_LOCHDR];
+                            if (end.endpos < ZIP64_LOCHDR ||
+                                readFullyAt(loc64, 0, loc64.length, end.endpos - ZIP64_LOCHDR)
+                                != loc64.length || GETSIG(loc64) != ZIP64_LOCSIG) {
+                                return end;
+                            }
+                            long end64pos = ZIP64_LOCOFF(loc64);
+                            byte[] end64buf = new byte[ZIP64_ENDHDR];
+                            if (readFullyAt(end64buf, 0, end64buf.length, end64pos)
+                                != end64buf.length || GETSIG(end64buf) != ZIP64_ENDSIG) {
+                                return end;
+                            }
+                            // end64 candidate found,
+                            long cenlen64 = ZIP64_ENDSIZ(end64buf);
+                            long cenoff64 = ZIP64_ENDOFF(end64buf);
+                            long centot64 = ZIP64_ENDTOT(end64buf);
+                            // double-check
+                            if (cenlen64 != end.cenlen && end.cenlen != ZIP64_MAGICVAL ||
+                                cenoff64 != end.cenoff && end.cenoff != ZIP64_MAGICVAL ||
+                                centot64 != end.centot && end.centot != ZIP64_MAGICCOUNT) {
+                                return end;
+                            }
+                            // to use the end64 values
+                            end.cenlen = cenlen64;
+                            end.cenoff = cenoff64;
+                            end.centot = (int)centot64; // assume total < 2g
+                            end.endpos = end64pos;
+                        } catch (IOException x) {}    // no zip64 loc/end
+                        return end;
+                    }
+                }
+            }
+            throw new ZipException("zip END header not found");
+        }
+
+        // Reads zip file central directory.
+        private void initCEN(int knownTotal) throws IOException {
+            // Prefer locals for better performance during startup
+            byte[] cen;
+            if (knownTotal == -1) {
+                End end = findEND();
+                if (end.endpos == 0) {
+                    locpos = 0;
+                    total = 0;
+                    entries = new int[0];
+                    this.cen = null;
+                    return;         // only END header present
+                }
+                if (end.cenlen > end.endpos)
+                    zerror("invalid END header (bad central directory size)");
+                long cenpos = end.endpos - end.cenlen;     // position of CEN table
+                // Get position of first local file (LOC) header, taking into
+                // account that there may be a stub prefixed to the zip file.
+                locpos = cenpos - end.cenoff;
+                if (locpos < 0) {
+                    zerror("invalid END header (bad central directory offset)");
+                }
+                // read in the CEN and END
+                cen = this.cen = new byte[(int)(end.cenlen + ENDHDR)];
+                if (readFullyAt(cen, 0, cen.length, cenpos) != end.cenlen + ENDHDR) {
+                    zerror("read CEN tables failed");
+                }
+                this.total = end.centot;
+            } else {
+                cen = this.cen;
+                this.total = knownTotal;
+            }
+            // hash table for entries
+            int entriesLength = this.total * 3;
+            entries = new int[entriesLength];
+
+            int tablelen = ((total/2) | 1); // Odd -> fewer collisions
+            this.tablelen = tablelen;
+
+            int[] table = new int[tablelen];
+            this.table = table;
+
+            Arrays.fill(table, ZIP_ENDCHAIN);
+
+            // ScummVM-changed: Don't support JAR
+            /*
+            // list for all meta entries
+            ArrayList<Integer> signatureNames = null;
+            // Set of all version numbers seen in META-INF/versions/
+            Set<Integer> metaVersionsSet = null;
+            */
+
+            // Iterate through the entries in the central directory
+            int idx = 0; // Index into the entries array
+            int pos = 0;
+            int entryPos = CENHDR;
+            int limit = cen.length - ENDHDR;
+            // ScummVM-changed: Don't support JAR
+            /*
+            manifestNum = 0;
+            */
+            // Android-added: duplicate entries are not allowed. See CVE-2013-4787 and b/8219321
+            Set<String> entriesNames = new HashSet<>();
+            while (entryPos <= limit) {
+                if (idx >= entriesLength) {
+                    // This will only happen if the zip file has an incorrect
+                    // ENDTOT field, which usually means it contains more than
+                    // 65535 entries.
+                    initCEN(countCENHeaders(cen, limit));
+                    return;
+                }
+
+                // Checks the entry and adds values to entries[idx ... idx+2]
+                int nlen = checkAndAddEntry(pos, idx);
+
+                // BEGIN Android-added: duplicate entries are not allowed. See CVE-2013-4787
+                // and b/8219321.
+                // zipCoderForPos takes USE_UTF8 flag into account.
+                ZipCoder zcp = zipCoderForPos(entryPos);
+                String name = zcp.toString(cen, pos + CENHDR, nlen);
+                if (!entriesNames.add(name)) {
+                    zerror("Duplicate entry name: " + name);
+                }
+                // END Android-added: duplicate entries are not allowed. See CVE-2013-4787
+                // and b/8219321
+                // BEGIN Android-added: don't allow NUL in entry names. We can handle it in Java fine,
+                // but it is of questionable utility as a valid pathname can't contain NUL.
+                for (int nameIdx = 0; nameIdx < nlen; ++nameIdx) {
+                    byte b = cen[pos + CENHDR + nameIdx];
+
+                    if (b == 0) {
+                        zerror("Filename contains NUL byte: " + name);
+                    }
+                }
+                // END Android-added: don't allow NUL in entry names.
+                // BEGIN Android-changed: validation of zip entry names.
+                // ScummVM-changed: use FileInputStream and don't use internal APIs.
+                /*
+                if (key.isZipFilePathValidatorEnabled && !ZipPathValidator.isClear()) {
+                    ZipPathValidator.getInstance().onZipEntryAccess(name);
+                }
+                */
+                // END Android-changed: validation of zip entry names.
+                idx += 3;
+
+                // ScummVM-changed: Don't support JAR
+                /*
+                // Adds name to metanames.
+                if (isMetaName(cen, entryPos, nlen)) {
+                    // nlen is at least META_INF_LENGTH
+                    if (isManifestName(entryPos + META_INF_LEN, nlen - META_INF_LEN)) {
+                        manifestPos = pos;
+                        manifestNum++;
+                    } else {
+                        if (isSignatureRelated(entryPos, nlen)) {
+                            if (signatureNames == null)
+                                signatureNames = new ArrayList<>(4);
+                            signatureNames.add(pos);
+                        }
+
+                        // If this is a versioned entry, parse the version
+                        // and store it for later. This optimizes lookup
+                        // performance in multi-release jar files
+                        int version = getMetaVersion(entryPos + META_INF_LEN, nlen - META_INF_LEN);
+                        if (version > 0) {
+                            if (metaVersionsSet == null)
+                                metaVersionsSet = new TreeSet<>();
+                            metaVersionsSet.add(version);
+                        }
+                    }
+                }
+                */
+                // skip to the start of the next entry
+                pos = nextEntryPos(pos, entryPos, nlen);
+                entryPos = pos + CENHDR;
+            }
+
+            // Adjust the total entries
+            this.total = idx / 3;
+
+            // ScummVM-changed: Don't support JAR
+            /*
+            if (signatureNames != null) {
+                int len = signatureNames.size();
+                signatureMetaNames = new int[len];
+                for (int j = 0; j < len; j++) {
+                    signatureMetaNames[j] = signatureNames.get(j);
+                }
+            }
+            if (metaVersionsSet != null) {
+                metaVersions = new int[metaVersionsSet.size()];
+                int c = 0;
+                for (Integer version : metaVersionsSet) {
+                    metaVersions[c++] = version;
+                }
+            } else {
+                metaVersions = EMPTY_META_VERSIONS;
+            }
+            */
+            if (pos + ENDHDR != cen.length) {
+                zerror("invalid CEN header (bad header size)");
+            }
+        }
+
+        private int nextEntryPos(int pos, int entryPos, int nlen) {
+            return entryPos + nlen + CENCOM(cen, pos) + CENEXT(cen, pos);
+        }
+
+        private static void zerror(String msg) throws ZipException {
+            throw new ZipException(msg);
+        }
+
+        /*
+         * Returns the {@code pos} of the zip cen entry corresponding to the
+         * specified entry name, or -1 if not found.
+         */
+        private int getEntryPos(String name, boolean addSlash) {
+            if (total == 0) {
+                return -1;
+            }
+
+            int hsh = ZipCoder.hash(name);
+            int idx = table[(hsh & 0x7fffffff) % tablelen];
+
+            // Search down the target hash chain for a entry whose
+            // 32 bit hash matches the hashed name.
+            while (idx != ZIP_ENDCHAIN) {
+                if (getEntryHash(idx) == hsh) {
+                    // The CEN name must match the specfied one
+                    int pos = getEntryPos(idx);
+
+                    try {
+                        ZipCoder zc = zipCoderForPos(pos);
+                        String entry = zc.toString(cen, pos + CENHDR, CENNAM(cen, pos));
+
+                        // If addSlash is true we'll test for name+/ in addition to
+                        // name, unless name is the empty string or already ends with a
+                        // slash
+                        int entryLen = entry.length();
+                        int nameLen = name.length();
+                        if ((entryLen == nameLen && entry.equals(name)) ||
+                                (addSlash &&
+                                nameLen + 1 == entryLen &&
+                                entry.startsWith(name) &&
+                                entry.charAt(entryLen - 1) == '/')) {
+                            return pos;
+                        }
+                    } catch (IllegalArgumentException iae) {
+                        // Ignore
+                    }
+                }
+                idx = getEntryNext(idx);
+            }
+            return -1;
+        }
+
+        private ZipCoder zipCoderForPos(int pos) {
+            if (zc.isUTF8()) {
+                return zc;
+            }
+            if ((CENFLG(cen, pos) & USE_UTF8) != 0) {
+                return ZipCoder.UTF8;
+            }
+            return zc;
+        }
+
+        /**
+         * Returns true if the bytes represent a non-directory name
+         * beginning with "META-INF/", disregarding ASCII case.
+         */
+        private static boolean isMetaName(byte[] name, int off, int len) {
+            // Use the "oldest ASCII trick in the book":
+            // ch | 0x20 == Character.toLowerCase(ch)
+            return len > META_INF_LEN       // "META-INF/".length()
+                && name[off + len - 1] != '/'  // non-directory
+                && (name[off++] | 0x20) == 'm'
+                && (name[off++] | 0x20) == 'e'
+                && (name[off++] | 0x20) == 't'
+                && (name[off++] | 0x20) == 'a'
+                && (name[off++]       ) == '-'
+                && (name[off++] | 0x20) == 'i'
+                && (name[off++] | 0x20) == 'n'
+                && (name[off++] | 0x20) == 'f'
+                && (name[off]         ) == '/';
+        }
+
+        /*
+         * Check if the bytes represents a name equals to MANIFEST.MF
+         */
+        private boolean isManifestName(int off, int len) {
+            byte[] name = cen;
+            return (len == 11 // "MANIFEST.MF".length()
+                    && (name[off++] | 0x20) == 'm'
+                    && (name[off++] | 0x20) == 'a'
+                    && (name[off++] | 0x20) == 'n'
+                    && (name[off++] | 0x20) == 'i'
+                    && (name[off++] | 0x20) == 'f'
+                    && (name[off++] | 0x20) == 'e'
+                    && (name[off++] | 0x20) == 's'
+                    && (name[off++] | 0x20) == 't'
+                    && (name[off++]       ) == '.'
+                    && (name[off++] | 0x20) == 'm'
+                    && (name[off]   | 0x20) == 'f');
+        }
+
+        // ScummVM-changed: Don't support JAR
+        /*
+        private boolean isSignatureRelated(int off, int len) {
+            // Only called when isMetaName(name, off, len) is true, which means
+            // len is at least META_INF_LENGTH
+            // assert isMetaName(name, off, len)
+            boolean signatureRelated = false;
+            byte[] name = cen;
+            if (name[off + len - 3] == '.') {
+                // Check if entry ends with .EC and .SF
+                int b1 = name[off + len - 2] | 0x20;
+                int b2 = name[off + len - 1] | 0x20;
+                if ((b1 == 'e' && b2 == 'c') || (b1 == 's' && b2 == 'f')) {
+                    signatureRelated = true;
+                }
+            } else if (name[off + len - 4] == '.') {
+                // Check if entry ends with .DSA and .RSA
+                int b1 = name[off + len - 3] | 0x20;
+                int b2 = name[off + len - 2] | 0x20;
+                int b3 = name[off + len - 1] | 0x20;
+                if ((b1 == 'r' || b1 == 'd') && b2 == 's' && b3 == 'a') {
+                    signatureRelated = true;
+                }
+            }
+            // Above logic must match SignatureFileVerifier.isBlockOrSF
+            assert(signatureRelated == SignatureFileVerifier
+                // Android-changed: use StandardCharsets.
+                // .isBlockOrSF(new String(name, off, len, UTF_8.INSTANCE)
+                .isBlockOrSF(new String(name, off, len, StandardCharsets.UTF_8)
+                    .toUpperCase(Locale.ENGLISH)));
+            return signatureRelated;
+        }
+        */
+
+        /*
+         * If the bytes represents a non-directory name beginning
+         * with "versions/", continuing with a positive integer,
+         * followed by a '/', then return that integer value.
+         * Otherwise, return 0
+         */
+        private int getMetaVersion(int off, int len) {
+            byte[] name = cen;
+            int nend = off + len;
+            if (!(len > 10                         // "versions//".length()
+                    && name[off + len - 1] != '/'  // non-directory
+                    && (name[off++] | 0x20) == 'v'
+                    && (name[off++] | 0x20) == 'e'
+                    && (name[off++] | 0x20) == 'r'
+                    && (name[off++] | 0x20) == 's'
+                    && (name[off++] | 0x20) == 'i'
+                    && (name[off++] | 0x20) == 'o'
+                    && (name[off++] | 0x20) == 'n'
+                    && (name[off++] | 0x20) == 's'
+                    && (name[off++]       ) == '/')) {
+                return 0;
+            }
+            int version = 0;
+            while (off < nend) {
+                final byte c = name[off++];
+                if (c == '/') {
+                    return version;
+                }
+                if (c < '0' || c > '9') {
+                    return 0;
+                }
+                version = version * 10 + c - '0';
+                // Check for overflow and leading zeros
+                if (version <= 0) {
+                    return 0;
+                }
+            }
+            return 0;
+        }
+
+        /**
+         * Returns the number of CEN headers in a central directory.
+         * Will not throw, even if the zip file is corrupt.
+         *
+         * @param cen copy of the bytes in a zip file's central directory
+         * @param size number of bytes in central directory
+         */
+        private static int countCENHeaders(byte[] cen, int size) {
+            int count = 0;
+            for (int p = 0;
+                 p + CENHDR <= size;
+                 p += CENHDR + CENNAM(cen, p) + CENEXT(cen, p) + CENCOM(cen, p))
+                count++;
+            return count;
+        }
+    }
+}
diff --git a/backends/platform/android/org/scummvm/scummvm/zip/ZipUtils.java b/backends/platform/android/org/scummvm/scummvm/zip/ZipUtils.java
new file mode 100644
index 00000000000..777cb8cb48d
--- /dev/null
+++ b/backends/platform/android/org/scummvm/scummvm/zip/ZipUtils.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (c) 2013, 2020, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package org.scummvm.scummvm.zip;
+
+// ScummVM-changed: improve compatibility.
+import java.io.IOException;
+import java.nio.ByteBuffer;
+// BEGIN ScummVM-changed: improve compatibility.
+import java.nio.charset.Charset;
+/*
+import java.nio.file.attribute.FileTime;
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+*/
+import java.util.Date;
+/*
+import java.util.concurrent.TimeUnit;
+*/
+import java.util.GregorianCalendar;
+// END ScummVM-changed: improve compatibility.
+
+import static org.scummvm.scummvm.zip.ZipConstants.ENDHDR;
+
+// ScummVM-changed: don't use internal APIs.
+//import jdk.internal.misc.Unsafe;
+
+class ZipUtils {
+
+    // ScummVM-changed: improve compatibility.
+    static final Charset UTF_8 = Charset.defaultCharset();
+
+    // used to adjust values between Windows and java epoch
+    private static final long WINDOWS_EPOCH_IN_MICROSECONDS = -11644473600000000L;
+
+    // used to indicate the corresponding windows time is not available
+    public static final long WINDOWS_TIME_NOT_AVAILABLE = Long.MIN_VALUE;
+
+    // static final ByteBuffer defaultBuf = ByteBuffer.allocateDirect(0);
+    static final ByteBuffer defaultBuf = ByteBuffer.allocate(0);
+
+    /**
+     * Converts Windows time (in microseconds, UTC/GMT) time to FileTime.
+     */
+    // ScummVM-changed: improve compatibility.
+    /*
+    public static final FileTime winTimeToFileTime(long wtime) {
+        return FileTime.from(wtime / 10 + WINDOWS_EPOCH_IN_MICROSECONDS,
+                             TimeUnit.MICROSECONDS);
+    }
+    */
+
+    /**
+     * Converts FileTime to Windows time.
+     */
+    // ScummVM-changed: improve compatibility.
+    /*
+    public static final long fileTimeToWinTime(FileTime ftime) {
+        return (ftime.to(TimeUnit.MICROSECONDS) - WINDOWS_EPOCH_IN_MICROSECONDS) * 10;
+    }
+    */
+
+    /**
+     * The upper bound of the 32-bit unix time, the "year 2038 problem".
+     */
+    public static final long UPPER_UNIXTIME_BOUND = 0x7fffffff;
+
+    /**
+     * Converts "standard Unix time"(in seconds, UTC/GMT) to FileTime
+     */
+    // ScummVM-changed: improve compatibility.
+    /*
+    public static final FileTime unixTimeToFileTime(long utime) {
+        return FileTime.from(utime, TimeUnit.SECONDS);
+    }
+    */
+
+    /**
+     * Converts FileTime to "standard Unix time".
+     */
+    // ScummVM-changed: improve compatibility.
+    /*
+    public static final long fileTimeToUnixTime(FileTime ftime) {
+        return ftime.to(TimeUnit.SECONDS);
+    }
+    */
+
+    /**
+     * Converts DOS time to Java time (number of milliseconds since epoch).
+     */
+    public static long dosToJavaTime(long dtime) {
+        int year = (int) (((dtime >> 25) & 0x7f) + 1980);
+        int month = (int) ((dtime >> 21) & 0x0f);
+        int day = (int) ((dtime >> 16) & 0x1f);
+        int hour = (int) ((dtime >> 11) & 0x1f);
+        int minute = (int) ((dtime >> 5) & 0x3f);
+        int second = (int) ((dtime << 1) & 0x3e);
+
+        if (month > 0 && month < 13 && day > 0 && hour < 24 && minute < 60 && second < 60) {
+            // ScummVM-changed: improve compatibility.
+            /*
+            try {
+                LocalDateTime ldt = LocalDateTime.of(year, month, day, hour, minute, second);
+                return TimeUnit.MILLISECONDS.convert(ldt.toEpochSecond(
+                        ZoneId.systemDefault().getRules().getOffset(ldt)), TimeUnit.SECONDS);
+            } catch (DateTimeException dte) {
+                // ignore
+            }
+            */
+            return (new GregorianCalendar(year, month, day, hour, minute, second)).getTimeInMillis();
+        }
+        return overflowDosToJavaTime(year, month, day, hour, minute, second);
+    }
+
+    /*
+     * Deal with corner cases where an arguably mal-formed DOS time is used
+     */
+    @SuppressWarnings("deprecation") // Use of Date constructor
+    private static long overflowDosToJavaTime(int year, int month, int day,
+                                              int hour, int minute, int second) {
+        return new Date(year - 1900, month - 1, day, hour, minute, second).getTime();
+    }
+
+
+    /**
+     * Converts extended DOS time to Java time, where up to 1999 milliseconds
+     * might be encoded into the upper half of the returned long.
+     *
+     * @param xdostime the extended DOS time value
+     * @return milliseconds since epoch
+     */
+    public static long extendedDosToJavaTime(long xdostime) {
+        long time = dosToJavaTime(xdostime);
+        return time + (xdostime >> 32);
+    }
+
+    /**
+     * Converts Java time to DOS time.
+     */
+    // ScummVM-changed: improve compatibility.
+    /*
+    private static long javaToDosTime(LocalDateTime ldt) {
+        int year = ldt.getYear() - 1980;
+        return (year << 25 |
+            ldt.getMonthValue() << 21 |
+            ldt.getDayOfMonth() << 16 |
+            ldt.getHour() << 11 |
+            ldt.getMinute() << 5 |
+            ldt.getSecond() >> 1) & 0xffffffffL;
+    }
+    */
+
+    /**
+     * Converts Java time to DOS time, encoding any milliseconds lost
+     * in the conversion into the upper half of the returned long.
+     *
+     * @param time milliseconds since epoch
+     * @return DOS time with 2s remainder encoded into upper half
+     */
+    // ScummVM-changed: improve compatibility.
+    /*
+    static long javaToExtendedDosTime(long time) {
+        LocalDateTime ldt = javaEpochToLocalDateTime(time);
+        if (ldt.getYear() >= 1980) {
+            return javaToDosTime(ldt) + ((time % 2000) << 32);
+        }
+        return ZipEntry.DOSTIME_BEFORE_1980;
+    }
+
+    static LocalDateTime javaEpochToLocalDateTime(long time) {
+        Instant instant = Instant.ofEpochMilli(time);
+        return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
+    }
+    */
+
+    /**
+     * Fetches unsigned 16-bit value from byte array at specified offset.
+     * The bytes are assumed to be in Intel (little-endian) byte order.
+     */
+    public static final int get16(byte b[], int off) {
+        return (b[off] & 0xff) | ((b[off + 1] & 0xff) << 8);
+    }
+
+    /**
+     * Fetches unsigned 32-bit value from byte array at specified offset.
+     * The bytes are assumed to be in Intel (little-endian) byte order.
+     */
+    public static final long get32(byte b[], int off) {
+        return (get16(b, off) | ((long)get16(b, off+2) << 16)) & 0xffffffffL;
+    }
+
+    /**
+     * Fetches signed 64-bit value from byte array at specified offset.
+     * The bytes are assumed to be in Intel (little-endian) byte order.
+     */
+    public static final long get64(byte b[], int off) {
+        return get32(b, off) | (get32(b, off+4) << 32);
+    }
+
+    /**
+     * Fetches signed 32-bit value from byte array at specified offset.
+     * The bytes are assumed to be in Intel (little-endian) byte order.
+     *
+     */
+    public static final int get32S(byte b[], int off) {
+        return (get16(b, off) | (get16(b, off+2) << 16));
+    }
+
+    // fields access methods
+    static final int CH(byte[] b, int n) {
+        return b[n] & 0xff ;
+    }
+
+    static final int SH(byte[] b, int n) {
+        return (b[n] & 0xff) | ((b[n + 1] & 0xff) << 8);
+    }
+
+    static final long LG(byte[] b, int n) {
+        return ((SH(b, n)) | (SH(b, n + 2) << 16)) & 0xffffffffL;
+    }
+
+    static final long LL(byte[] b, int n) {
+        return (LG(b, n)) | (LG(b, n + 4) << 32);
+    }
+
+    static final long GETSIG(byte[] b) {
+        return LG(b, 0);
+    }
+
+    /*
+     * File attribute compatibility types of CEN field "version made by"
+     */
+    static final int FILE_ATTRIBUTES_UNIX = 3; // Unix
+
+    /*
+     * Base values for CEN field "version made by"
+     */
+    static final int VERSION_MADE_BY_BASE_UNIX = FILE_ATTRIBUTES_UNIX << 8; // Unix
+
+
+    // local file (LOC) header fields
+    static final long LOCSIG(byte[] b) { return LG(b, 0); } // signature
+    static final int  LOCVER(byte[] b) { return SH(b, 4); } // version needed to extract
+    static final int  LOCFLG(byte[] b) { return SH(b, 6); } // general purpose bit flags
+    static final int  LOCHOW(byte[] b) { return SH(b, 8); } // compression method
+    static final long LOCTIM(byte[] b) { return LG(b, 10);} // modification time
+    static final long LOCCRC(byte[] b) { return LG(b, 14);} // crc of uncompressed data
+    static final long LOCSIZ(byte[] b) { return LG(b, 18);} // compressed data size
+    static final long LOCLEN(byte[] b) { return LG(b, 22);} // uncompressed data size
+    static final int  LOCNAM(byte[] b) { return SH(b, 26);} // filename length
+    static final int  LOCEXT(byte[] b) { return SH(b, 28);} // extra field length
+
+    // extra local (EXT) header fields
+    static final long EXTCRC(byte[] b) { return LG(b, 4);}  // crc of uncompressed data
+    static final long EXTSIZ(byte[] b) { return LG(b, 8);}  // compressed size
+    static final long EXTLEN(byte[] b) { return LG(b, 12);} // uncompressed size
+
+    // end of central directory header (END) fields
+    static final int  ENDSUB(byte[] b) { return SH(b, 8); }  // number of entries on this disk
+    static final int  ENDTOT(byte[] b) { return SH(b, 10);}  // total number of entries
+    static final long ENDSIZ(byte[] b) { return LG(b, 12);}  // central directory size
+    static final long ENDOFF(byte[] b) { return LG(b, 16);}  // central directory offset
+    static final int  ENDCOM(byte[] b) { return SH(b, 20);}  // size of zip file comment
+    static final int  ENDCOM(byte[] b, int off) { return SH(b, off + 20);}
+
+    // zip64 end of central directory recoder fields
+    static final long ZIP64_ENDTOD(byte[] b) { return LL(b, 24);}  // total number of entries on disk
+    static final long ZIP64_ENDTOT(byte[] b) { return LL(b, 32);}  // total number of entries
+    static final long ZIP64_ENDSIZ(byte[] b) { return LL(b, 40);}  // central directory size
+    static final long ZIP64_ENDOFF(byte[] b) { return LL(b, 48);}  // central directory offset
+    static final long ZIP64_LOCOFF(byte[] b) { return LL(b, 8);}   // zip64 end offset
+
+    // central directory header (CEN) fields
+    static final long CENSIG(byte[] b, int pos) { return LG(b, pos + 0); }
+    static final int  CENVEM(byte[] b, int pos) { return SH(b, pos + 4); }
+    static final int  CENVEM_FA(byte[] b, int pos) { return CH(b, pos + 5); } // file attribute compatibility
+    static final int  CENVER(byte[] b, int pos) { return SH(b, pos + 6); }
+    static final int  CENFLG(byte[] b, int pos) { return SH(b, pos + 8); }
+    static final int  CENHOW(byte[] b, int pos) { return SH(b, pos + 10);}
+    static final long CENTIM(byte[] b, int pos) { return LG(b, pos + 12);}
+    static final long CENCRC(byte[] b, int pos) { return LG(b, pos + 16);}
+    static final long CENSIZ(byte[] b, int pos) { return LG(b, pos + 20);}
+    static final long CENLEN(byte[] b, int pos) { return LG(b, pos + 24);}
+    static final int  CENNAM(byte[] b, int pos) { return SH(b, pos + 28);}
+    static final int  CENEXT(byte[] b, int pos) { return SH(b, pos + 30);}
+    static final int  CENCOM(byte[] b, int pos) { return SH(b, pos + 32);}
+    static final int  CENDSK(byte[] b, int pos) { return SH(b, pos + 34);}
+    static final int  CENATT(byte[] b, int pos) { return SH(b, pos + 36);}
+    static final long CENATX(byte[] b, int pos) { return LG(b, pos + 38);}
+    static final int  CENATX_PERMS(byte[] b, int pos) { return SH(b, pos + 40);} // posix permission data
+    static final long CENOFF(byte[] b, int pos) { return LG(b, pos + 42);}
+
+    // The END header is followed by a variable length comment of size < 64k.
+    static final long END_MAXLEN = 0xFFFF + ENDHDR;
+    static final int READBLOCKSZ = 128;
+
+    // Android-removed: not available on Android.
+    /*
+     * Loads zip native library, if not already laoded
+     *
+    static void loadLibrary() {
+        jdk.internal.loader.BootLoader.loadLibrary("zip");
+    }
+    */
+
+    // ScummVM-changed: don't use internal APIs.
+    /*
+    private static final Unsafe unsafe = Unsafe.getUnsafe();
+
+    private static final long byteBufferArrayOffset = unsafe.objectFieldOffset(ByteBuffer.class, "hb");
+    private static final long byteBufferOffsetOffset = unsafe.objectFieldOffset(ByteBuffer.class, "offset");
+
+    static byte[] getBufferArray(ByteBuffer byteBuffer) {
+        return (byte[]) unsafe.getReference(byteBuffer, byteBufferArrayOffset);
+    }
+
+    static int getBufferOffset(ByteBuffer byteBuffer) {
+        return unsafe.getInt(byteBuffer, byteBufferOffsetOffset);
+    }
+    */
+
+    // ScummVM-changed: improve compatibility.
+    static class UncheckedIOException extends RuntimeException {
+        UncheckedIOException(IOException ioe) {
+            super(ioe);
+        }
+
+        public IOException getCause() {
+            return (IOException) super.getCause();
+        }
+    }
+}


Commit: c9985254432d5e018f1a3a50dac5750444f86af2
    https://github.com/scummvm/scummvm/commit/c9985254432d5e018f1a3a50dac5750444f86af2
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Use our own ZipFile implementation and load icons when needed

Changed paths:
    backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
    dists/android/res/layout/shortcut_creator_activity.xml


diff --git a/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java b/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
index 2ae006e742e..46ce009dfab 100644
--- a/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ShortcutCreatorActivity.java
@@ -12,11 +12,10 @@ import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.ParcelFileDescriptor;
 import android.text.Editable;
 import android.text.TextWatcher;
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -28,43 +27,33 @@ import android.widget.ArrayAdapter;
 import android.widget.EditText;
 import android.widget.ImageView;
 import android.widget.ListView;
-import android.widget.ProgressBar;
 import android.widget.TextView;
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 
+import org.scummvm.scummvm.zip.ZipEntry;
+import org.scummvm.scummvm.zip.ZipFile;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileReader;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipException;
-import java.util.zip.ZipInputStream;
 
-public class ShortcutCreatorActivity extends Activity {
-	private IconsCache _cache;
-	private GameAdapter _listAdapter;
 
-	private final ExecutorService _executor = new ThreadPoolExecutor(
-		0, Runtime.getRuntime().availableProcessors(),
-		1L, TimeUnit.SECONDS,
-		new LinkedBlockingQueue<>());
+public class ShortcutCreatorActivity extends Activity {
+	final protected static String LOG_TAG = "ShortcutCreatorActivity";
 
-	private ProgressBar _progressBar;
+	private IconsCache _cache;
 
 	@Override
 	protected void onCreate(Bundle savedInstanceState) {
@@ -95,30 +84,27 @@ public class ShortcutCreatorActivity extends Activity {
 
 		games = Game.loadGames(parsedIniMap);
 
-		OpenFileResult defaultStream = openFile(new File(getFilesDir(), "gui-icons.dat"));
+		FileInputStream defaultStream = openFile(new File(getFilesDir(), "gui-icons.dat"));
 
 		File iconsPath = INIParser.getPath(parsedIniMap, "scummvm", "iconspath",
 			new File(getFilesDir(), "icons"));
-		OpenFileResult[] packsStream = openFiles(iconsPath, "gui-icons.*\\.dat");
+		FileInputStream[] packsStream = openFiles(iconsPath, "gui-icons.*\\.dat");
+
+		_cache = new IconsCache(this, defaultStream, packsStream);
 
-		_cache = new IconsCache(this, _cacheListener,
-			games, defaultStream, packsStream,
-			_executor, new Handler(Looper.getMainLooper()));
-		_listAdapter = new GameAdapter(this, games, _cache);
+		final GameAdapter listAdapter = new GameAdapter(this, games, _cache);
 
 		ListView listView = findViewById(R.id.shortcut_creator_games_list);
-		listView.setAdapter(_listAdapter);
+		listView.setAdapter(listAdapter);
 		listView.setEmptyView(findViewById(R.id.shortcut_creator_games_list_empty));
 		listView.setOnItemClickListener(_gameClicked);
 
-		_progressBar = findViewById(R.id.shortcut_creator_progress_bar);
-
 		EditText searchEdit = findViewById(R.id.shortcut_creator_search_edit);
 		searchEdit.addTextChangedListener(new TextWatcher() {
 
 			@Override
 			public void onTextChanged(CharSequence cs, int arg1, int arg2, int arg3) {
-				_listAdapter.getFilter().filter(cs.toString());
+				listAdapter.getFilter().filter(cs.toString());
 			}
 
 			@Override
@@ -137,42 +123,24 @@ public class ShortcutCreatorActivity extends Activity {
 		setResult(RESULT_CANCELED);
 	}
 
-	@Override
-	protected void onDestroy() {
-		super.onDestroy();
-
-		_executor.shutdownNow();
-	}
-
-	private static class OpenFileResult {
-		@NonNull
-		public FileInputStream stream;
-		public long streamSize;
-		OpenFileResult(@NonNull FileInputStream stream, long streamSize) {
-			this.stream = stream;
-			this.streamSize = streamSize;
-		}
-	}
-
-	private OpenFileResult openFile(File path) {
+	private FileInputStream openFile(File path) {
 		 try {
-			FileInputStream stream = new FileInputStream(path);
-			return new OpenFileResult(stream, path.length());
+			return new FileInputStream(path);
 		} catch (FileNotFoundException e) {
 			return null;
 		}
 	}
 
-	private OpenFileResult[] openFiles(File basePath, String regex) {
+	private FileInputStream[] openFiles(File basePath, String regex) {
 		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
 			!basePath.getPath().startsWith("/saf/")) {
 			// This is a standard filesystem path
 			File[] children = basePath.listFiles((dir, name) -> name.matches(regex));
 			if (children == null) {
-				return new OpenFileResult[0];
+				return new FileInputStream[0];
 			}
 			Arrays.sort(children);
-			OpenFileResult[] ret = new OpenFileResult[children.length];
+			FileInputStream[] ret = new FileInputStream[children.length];
 			int i = 0;
 			for (File f: children) {
 				ret[i] = openFile(f);
@@ -191,19 +159,19 @@ public class ShortcutCreatorActivity extends Activity {
 
 		SAFFSTree tree = SAFFSTree.findTree(this, treeName);
 		if (tree == null) {
-			return new OpenFileResult[0];
+			return new FileInputStream[0];
 		}
 		SAFFSTree.SAFFSNode node = tree.pathToNode(path);
 		if (node == null) {
-			return new OpenFileResult[0];
+			return new FileInputStream[0];
 		}
 		SAFFSTree.SAFFSNode[] children = tree.getChildren(node);
 		if (children == null) {
-			return new OpenFileResult[0];
+			return new FileInputStream[0];
 		}
 		Arrays.sort(children);
 
-		ArrayList<OpenFileResult> ret = new ArrayList<>();
+		ArrayList<FileInputStream> ret = new ArrayList<>();
 		for (SAFFSTree.SAFFSNode child : children) {
 			if ((child._flags & SAFFSTree.SAFFSNode.DIRECTORY) != 0) {
 				continue;
@@ -216,12 +184,9 @@ public class ShortcutCreatorActivity extends Activity {
 			if (pfd == null) {
 				continue;
 			}
-			ret.add(new OpenFileResult(
-				new ParcelFileDescriptor.AutoCloseInputStream(pfd),
-				pfd.getStatSize()
-			));
+			ret.add(new ParcelFileDescriptor.AutoCloseInputStream(pfd));
 		}
-		return ret.toArray(new OpenFileResult[0]);
+		return ret.toArray(new FileInputStream[0]);
 	}
 
 	private final OnItemClickListener _gameClicked = new OnItemClickListener() {
@@ -271,21 +236,6 @@ public class ShortcutCreatorActivity extends Activity {
 		}
 	};
 
-	private final IconsCache.IconsCacheListener _cacheListener = new IconsCache.IconsCacheListener() {
-		@Override
-		public void onIconUpdated(List<Game> games) {
-			_listAdapter.notifyDataSetChanged();
-		}
-
-		@Override
-		public void onLoadProgress(int percent) {
-			if (percent == 100) {
-				_progressBar.setVisibility(View.GONE);
-			}
-			_progressBar.setProgress(percent);
-		}
-	};
-
 	private static class Game {
 		@NonNull
 		private final String _target;
@@ -355,67 +305,43 @@ public class ShortcutCreatorActivity extends Activity {
 
 	private static class IconsCache {
 		/**
-		 * This kind of mimics Common::generateZipSet with asynchronous feature
+		 * This kind of mimics Common::generateZipSet
 		 */
-		public interface IconsCacheListener {
-			void onIconUpdated(List<Game> games);
-			void onLoadProgress(int percent);
-		}
-
 		private final Context _context;
-		private final IconsCacheListener _listener;
-		private final Map<String, byte[]> _icons = new HashMap<>();
-		private final Map<String, List<Game>> _candidates = new HashMap<>();
-
-		private long _totalSize;
-		private final long[] _totalSizes;
-		private final long[] _readSizes;
-
-		public IconsCache(Context context, IconsCacheListener listener,
-		                  List<Game> games,
-		                  OpenFileResult defaultStream, OpenFileResult[] packsStream,
-		                  Executor executor, Handler handler) {
-			_context = context;
-			_listener = listener;
-
-			// Establish a list of candidates
-			for (Game game : games) {
-				for (String candidate : game.getIconCandidates()) {
-					List<Game> v = _candidates.get(candidate);
-					if (v == null) {
-						v = new ArrayList<>();
-						_candidates.put(candidate, v);
-					}
-					v.add(game);
-				}
+		private final Map<String, byte[]> _icons = new LinkedHashMap<String, byte[]>(16,0.75f, true) {
+			@Override
+			protected boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {
+				return size() > 128;
 			}
+		};
+		private static final byte[] _noIconSentinel = new byte[0];
 
-			_totalSizes = new long[1 + packsStream.length];
-			_readSizes = new long[1 + packsStream.length];
+		private final List<ZipFile> _zipFiles = new ArrayList<>();
 
-			// Iterate over the files starting with default and continuing with packs
-			// This will let us erase outdated versions
-			if (defaultStream != null) {
-				_totalSizes[0] = defaultStream.streamSize;
-				_totalSize += _totalSizes[0];
-				executor.execute(() -> loadZip(defaultStream.stream, 0, handler));
-			}
-			int i = 1;
-			for (final OpenFileResult packStream : packsStream) {
+		public IconsCache(Context context,
+		                  FileInputStream defaultStream,
+		                  FileInputStream[] packsStream) {
+			_context = context;
+
+			for (int i = packsStream.length - 1; i >= 0; i--) {
+				final FileInputStream packStream = packsStream[i];
 				if (packStream == null) {
 					continue;
 				}
-
-				_totalSizes[i] = packStream.streamSize;
-				_totalSize += _totalSizes[i];
-				// Make it final for lambda
-				int argI = i;
-				executor.execute(() -> loadZip(packStream.stream, argI, handler));
-				i += 1;
+				try {
+					ZipFile zf = new ZipFile(packStream);
+					_zipFiles.add(zf);
+				} catch (IOException e) {
+					Log.e(LOG_TAG, "Error while loading pack ZipFile: " + i, e);
+				}
 			}
-
-			if (_totalSize == 0) {
-				handler.post(() -> _listener.onLoadProgress(100));
+			if (defaultStream != null) {
+				try {
+					ZipFile zf = new ZipFile(defaultStream);
+					_zipFiles.add(zf);
+				} catch (IOException e) {
+					Log.e(LOG_TAG, "Error while loading default ZipFile", e);
+				}
 			}
 		}
 
@@ -423,6 +349,9 @@ public class ShortcutCreatorActivity extends Activity {
 			for (String name : game.getIconCandidates()) {
 				byte[] data = _icons.get(name);
 				if (data == null) {
+					data = loadIcon(name);
+				}
+				if (data == _noIconSentinel) {
 					continue;
 				}
 
@@ -437,55 +366,35 @@ public class ShortcutCreatorActivity extends Activity {
 			return null;
 		}
 
-		private void loadZip(@NonNull FileInputStream zipStream, int id, @NonNull Handler handler) {
-			try (ZipInputStream zip = new ZipInputStream(zipStream)) {
-				ZipEntry entry;
-				while ((entry = zip.getNextEntry()) != null) {
-					_readSizes[id] = zipStream.getChannel().position();
-					handler.post(() -> {
-						long readSize = 0;
-						for (long pos : _readSizes) {
-							readSize += pos;
-						}
-						_listener.onLoadProgress((int)(readSize * 100 / _totalSize));
-					});
-
-					String name = entry.getName().toLowerCase();
-					if (entry.isDirectory()) {
-						zip.closeEntry();
-						continue;
-					}
-					if (!_candidates.containsKey(name)) {
-						zip.closeEntry();
-						continue;
-					}
+		private byte[] loadIcon(String name) {
+			int zfi = 0;
+			for(ZipFile zf : _zipFiles) {
+				final ZipEntry ze = zf.getEntry(name);
+				if (ze == null) {
+					zfi++;
+					continue;
+				}
 
-					int sz = (int) entry.getSize();
-					byte[] buffer = new byte[sz];
-					int off = 0;
-					while (off < buffer.length && (sz = zip.read(buffer, off, buffer.length - off)) > 0) {
-						off += sz;
-					}
-					if (off != buffer.length) {
+				int sz = (int) ze.getSize();
+				byte[] buffer = new byte[sz];
+
+				try (InputStream is = zf.getInputStream(ze)) {
+					if (is.read(buffer) != buffer.length) {
 						throw new IOException();
 					}
-
-					_icons.put(name, buffer);
-					handler.post(() -> _listener.onIconUpdated(_candidates.get(name)));
-
-					zip.closeEntry();
+				} catch (IOException e) {
+					Log.e(LOG_TAG, "Error while uncompressing: " + name + " from zip file " + zfi, e);
+					zfi++;
+					continue;
 				}
-				_readSizes[id] = _totalSizes[id];
-				handler.post(() -> {
-					long readSize = 0;
-					for (long pos : _readSizes) {
-						readSize += pos;
-					}
-					_listener.onLoadProgress((int)(readSize * 100 / _totalSize));
-				});
-			} catch (ZipException ignored) {
-			} catch (IOException ignored) {
+
+				_icons.put(name, buffer);
+				return buffer;
 			}
+
+			// Register failure
+			_icons.put(name, _noIconSentinel);
+			return _noIconSentinel;
 		}
 	}
 
diff --git a/dists/android/res/layout/shortcut_creator_activity.xml b/dists/android/res/layout/shortcut_creator_activity.xml
index 988971c1d33..b1d2e160f9e 100644
--- a/dists/android/res/layout/shortcut_creator_activity.xml
+++ b/dists/android/res/layout/shortcut_creator_activity.xml
@@ -47,12 +47,4 @@
 
 	</FrameLayout>
 
-	<ProgressBar
-		android:id="@+id/shortcut_creator_progress_bar"
-		style="?android:attr/progressBarStyleHorizontal"
-		android:layout_width="match_parent"
-		android:layout_height="wrap_content"
-		android:max="100"
-		tools:progress="50" />
-
 </LinearLayout>


Commit: e0eac792df6c954d47cc6a92c1b38dbe15dae66d
    https://github.com/scummvm/scummvm/commit/e0eac792df6c954d47cc6a92c1b38dbe15dae66d
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-06-01T12:44:11+02:00

Commit Message:
ANDROID: Fix typo in comment

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 30c90b32d0a..17e21ad5e70 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -1110,7 +1110,7 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 		// Make sure the thread is actively polling for events
 		_scummvm.setPause(false);
 		try {
-			// 1s timeout
+			// 2s timeout
 			_scummvm_thread.join(2000);
 		} catch (InterruptedException e) {
 			Log.i(ScummVM.LOG_TAG, "Error while joining ScummVM thread", e);
@@ -1189,7 +1189,7 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 			// Make sure the thread is actively polling for events
 			_scummvm.setPause(false);
 			try {
-				// 1s timeout
+				// 2s timeout
 				_scummvm_thread.join(2000);
 			} catch (InterruptedException e) {
 				Log.i(ScummVM.LOG_TAG, "Error while joining ScummVM thread", e);




More information about the Scummvm-git-logs mailing list