Initial commit
This commit is contained in:
519
backends/platform/android/org/scummvm/scummvm/BackupManager.java
Normal file
519
backends/platform/android/org/scummvm/scummvm/BackupManager.java
Normal file
@@ -0,0 +1,519 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.scummvm.scummvm.zip.ZipFile;
|
||||
|
||||
public class BackupManager {
|
||||
public static final int ERROR_CANCELLED = 1;
|
||||
public static final int ERROR_NO_ERROR = 0;
|
||||
public static final int ERROR_INVALID_BACKUP = -1;
|
||||
public static final int ERROR_INVALID_SAVES = -2;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public static int exportBackup(Context context, Uri output) {
|
||||
try (ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(output, "wt")) {
|
||||
if (pfd == null) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
return exportBackup(context, new FileOutputStream(pfd.getFileDescriptor()));
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
public static int exportBackup(Context context, File output) {
|
||||
try {
|
||||
return exportBackup(context, new FileOutputStream(output, false));
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public static int importBackup(ScummVMActivity context, Uri input) {
|
||||
try (ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(input, "r")) {
|
||||
if (pfd == null) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
return importBackup(context, new FileInputStream(pfd.getFileDescriptor()));
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
public static int importBackup(ScummVMActivity context, File input) {
|
||||
try {
|
||||
return importBackup(context, new FileInputStream(input));
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
private static int exportBackup(Context context, FileOutputStream output) {
|
||||
File configuration = new File(context.getFilesDir(), "scummvm.ini");
|
||||
|
||||
Map<String, Map<String, String>> parsedIniMap;
|
||||
try (FileReader reader = new FileReader(configuration)) {
|
||||
parsedIniMap = INIParser.parse(reader);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
parsedIniMap = null;
|
||||
} catch(IOException ignored) {
|
||||
parsedIniMap = null;
|
||||
}
|
||||
|
||||
if (parsedIniMap == null) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
ZipOutputStream zos = new ZipOutputStream(output);
|
||||
|
||||
try (FileInputStream stream = new FileInputStream(configuration)) {
|
||||
ZipEntry entry = new ZipEntry("scummvm.ini");
|
||||
entry.setSize(configuration.length());
|
||||
entry.setTime(configuration.lastModified());
|
||||
|
||||
zos.putNextEntry(entry);
|
||||
copyStream(zos, stream);
|
||||
zos.closeEntry();
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
try {
|
||||
ZipEntry entry;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
entry = new ZipEntry("saf");
|
||||
zos.putNextEntry(entry);
|
||||
if (!exportTrees(context, zos)) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
zos.closeEntry();
|
||||
}
|
||||
|
||||
entry = new ZipEntry("saves/");
|
||||
zos.putNextEntry(entry);
|
||||
zos.closeEntry();
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
File savesPath = INIParser.getPath(parsedIniMap, "scummvm", "savepath",
|
||||
new File(context.getFilesDir(), "saves"));
|
||||
boolean ret = exportSaves(context, savesPath, zos, "saves/");
|
||||
if (!ret) {
|
||||
try {
|
||||
zos.close();
|
||||
} catch(IOException ignored) {
|
||||
}
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
HashSet<File> savesPaths = new HashSet<>();
|
||||
savesPaths.add(savesPath);
|
||||
|
||||
int sectionId = -1;
|
||||
for (String section : parsedIniMap.keySet()) {
|
||||
sectionId++;
|
||||
if ("scummvm".equals(section)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
savesPath = INIParser.getPath(parsedIniMap, section, "savepath", null);
|
||||
if (savesPath == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (savesPaths.contains(savesPath)) {
|
||||
continue;
|
||||
}
|
||||
savesPaths.add(savesPath);
|
||||
|
||||
String folderName = "saves-" + sectionId + "/";
|
||||
ZipEntry entry = new ZipEntry(folderName);
|
||||
try {
|
||||
zos.putNextEntry(entry);
|
||||
zos.closeEntry();
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
ret = exportSaves(context, savesPath, zos, folderName);
|
||||
if (!ret) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
zos.close();
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
return ret ? ERROR_NO_ERROR : ERROR_INVALID_SAVES;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
static private boolean exportTrees(Context context, ZipOutputStream zos) throws IOException {
|
||||
ObjectOutputStream oos = new ObjectOutputStream(zos);
|
||||
// Version 1
|
||||
oos.writeInt(1);
|
||||
|
||||
SAFFSTree[] trees = SAFFSTree.getTrees(context);
|
||||
|
||||
oos.writeInt(trees.length);
|
||||
for (SAFFSTree tree : trees) {
|
||||
oos.writeObject(tree.getTreeName());
|
||||
oos.writeObject(tree.getTreeId());
|
||||
oos.writeObject(tree.getTreeDocumentUri().toString());
|
||||
}
|
||||
|
||||
// Don't close as it would close the underlying ZipOutputStream
|
||||
oos.flush();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static private boolean exportSaves(Context context, File folder, ZipOutputStream zos, String folderName) {
|
||||
SAFFSTree.PathResult pr;
|
||||
try {
|
||||
pr = SAFFSTree.fullPathToNode(context, folder.getPath(), false);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This version check is only to make Android Studio linter happy
|
||||
if (pr == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
// This is a standard filesystem path
|
||||
File[] children = folder.listFiles();
|
||||
if (children == null) {
|
||||
return true;
|
||||
}
|
||||
Arrays.sort(children);
|
||||
for (File f: children) {
|
||||
if ("timestamps".equals(f.getName())) {
|
||||
continue;
|
||||
}
|
||||
try (FileInputStream stream = new FileInputStream(f)) {
|
||||
ZipEntry entry = new ZipEntry(folderName + f.getName());
|
||||
entry.setSize(f.length());
|
||||
entry.setTime(f.lastModified());
|
||||
|
||||
zos.putNextEntry(entry);
|
||||
copyStream(zos, stream);
|
||||
zos.closeEntry();
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return false;
|
||||
} catch(IOException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// This is a SAF fake mount point
|
||||
SAFFSTree.SAFFSNode[] children = pr.tree.getChildren(pr.node);
|
||||
if (children == null) {
|
||||
return false;
|
||||
}
|
||||
Arrays.sort(children);
|
||||
|
||||
for (SAFFSTree.SAFFSNode child : children) {
|
||||
if ((child._flags & SAFFSTree.SAFFSNode.DIRECTORY) != 0) {
|
||||
continue;
|
||||
}
|
||||
String component = child._path.substring(child._path.lastIndexOf('/') + 1);
|
||||
if ("timestamps".equals(component)) {
|
||||
continue;
|
||||
}
|
||||
try (ParcelFileDescriptor pfd = pr.tree.createFileDescriptor(child, "r")) {
|
||||
ZipEntry entry = new ZipEntry(folderName + component);
|
||||
|
||||
zos.putNextEntry(entry);
|
||||
copyStream(zos, new ParcelFileDescriptor.AutoCloseInputStream(pfd));
|
||||
zos.closeEntry();
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return false;
|
||||
} catch(IOException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int importBackup(ScummVMActivity context, FileInputStream input) {
|
||||
ZipFile zf;
|
||||
try {
|
||||
zf = new ZipFile(input);
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
org.scummvm.scummvm.zip.ZipEntry ze = zf.getEntry("scummvm.ini");
|
||||
if (ze == null) {
|
||||
// No configuration file, not a backup
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
// Avoid using tmp suffix as it's used by atomic file support
|
||||
File configurationTmp = new File(context.getFilesDir(), "scummvm.ini.tmp2");
|
||||
|
||||
try (FileOutputStream os = new FileOutputStream(configurationTmp);
|
||||
InputStream is = zf.getInputStream(ze)) {
|
||||
copyStream(os, is);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
// Load the new configuration to know where to put saves
|
||||
Map<String, Map<String, String>> parsedIniMap;
|
||||
try (FileReader reader = new FileReader(configurationTmp)) {
|
||||
parsedIniMap = INIParser.parse(reader);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
parsedIniMap = null;
|
||||
} catch(IOException ignored) {
|
||||
parsedIniMap = null;
|
||||
}
|
||||
|
||||
if (parsedIniMap == null) {
|
||||
configurationTmp.delete();
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// Restore the missing SAF trees
|
||||
ze = zf.getEntry("saf");
|
||||
if (ze == null) {
|
||||
// No configuration file, not a backup
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
try (InputStream is = zf.getInputStream(ze)) {
|
||||
if (importTrees(context, is) == ERROR_INVALID_BACKUP) {
|
||||
// Try to continue except obvious error
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
// Move the configuration back now that we know it's parsable and that SAF is set up
|
||||
Log.i(ScummVM.LOG_TAG, "Writing new ScummVM configuration");
|
||||
File configuration = new File(context.getFilesDir(), "scummvm.ini");
|
||||
if (!configurationTmp.renameTo(configuration)) {
|
||||
try (FileOutputStream os = new FileOutputStream(configuration);
|
||||
FileInputStream is = new FileInputStream(configurationTmp)) {
|
||||
copyStream(os, is);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
configurationTmp.delete();
|
||||
}
|
||||
|
||||
File savesPath = INIParser.getPath(parsedIniMap, "scummvm", "savepath",
|
||||
new File(context.getFilesDir(), "saves"));
|
||||
|
||||
boolean ret = importSaves(context, savesPath, zf, "saves/");
|
||||
if (!ret) {
|
||||
try {
|
||||
zf.close();
|
||||
} catch(IOException ignored) {
|
||||
}
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
HashSet<File> savesPaths = new HashSet<>();
|
||||
savesPaths.add(savesPath);
|
||||
|
||||
int sectionId = -1;
|
||||
for (String section : parsedIniMap.keySet()) {
|
||||
sectionId++;
|
||||
if ("scummvm".equals(section)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
savesPath = INIParser.getPath(parsedIniMap, section, "savepath", null);
|
||||
if (savesPath == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (savesPaths.contains(savesPath)) {
|
||||
continue;
|
||||
}
|
||||
savesPaths.add(savesPath);
|
||||
|
||||
String folderName = "saves-" + sectionId + "/";
|
||||
ret = importSaves(context, savesPath, zf, folderName);
|
||||
if (!ret) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
zf.close();
|
||||
} catch(IOException ignored) {
|
||||
}
|
||||
|
||||
return ret ? ERROR_NO_ERROR : ERROR_INVALID_SAVES;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
static private int importTrees(ScummVMActivity context, InputStream is) throws IOException, ClassNotFoundException {
|
||||
boolean failed = false;
|
||||
|
||||
ObjectInputStream ois = new ObjectInputStream(is);
|
||||
// Version 1
|
||||
if (ois.readInt() != 1) {
|
||||
// Invalid version
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
for (int length = ois.readInt(); length > 0; length--) {
|
||||
String treeName = (String)ois.readObject();
|
||||
String treeId = (String)ois.readObject();
|
||||
String treeUri = (String)ois.readObject();
|
||||
|
||||
if (SAFFSTree.findTree(context, treeId) != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Uri uri = context.selectWithNativeUI(true, true, Uri.parse(treeUri), treeName, null, null);
|
||||
if (uri == null) {
|
||||
failed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Register the new selected tree
|
||||
SAFFSTree.newTree(context, uri);
|
||||
}
|
||||
|
||||
ois.close();
|
||||
|
||||
return failed ? ERROR_CANCELLED : ERROR_NO_ERROR;
|
||||
}
|
||||
|
||||
static private boolean importSaves(Context context, File folder, ZipFile zf, String folderName) {
|
||||
SAFFSTree.PathResult pr;
|
||||
try {
|
||||
pr = SAFFSTree.fullPathToNode(context, folder.getPath(), true);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This version check is only to make Android Studio linter happy
|
||||
if (pr == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
// This is a standard filesystem path
|
||||
if (!folder.isDirectory() && !folder.mkdirs()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Enumeration<? extends org.scummvm.scummvm.zip.ZipEntry> entries = zf.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
org.scummvm.scummvm.zip.ZipEntry entry = entries.nextElement();
|
||||
String name = entry.getName();
|
||||
if (!name.startsWith(folderName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the base name (this avoids directory traversal)
|
||||
name = name.substring(name.lastIndexOf("/") + 1);
|
||||
if (name.isEmpty() || "timestamps".equals(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
File f = new File(folder, name);
|
||||
try (InputStream is = zf.getInputStream(entry);
|
||||
FileOutputStream os = new FileOutputStream(f)) {
|
||||
copyStream(os, is);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return false;
|
||||
} catch(IOException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// This is a SAF fake mount point
|
||||
Enumeration<? extends org.scummvm.scummvm.zip.ZipEntry> entries = zf.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
org.scummvm.scummvm.zip.ZipEntry entry = entries.nextElement();
|
||||
String name = entry.getName();
|
||||
if (!name.startsWith(folderName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the base name (this avoids directory traversal)
|
||||
name = name.substring(name.lastIndexOf("/") + 1);
|
||||
if (name.isEmpty() || "timestamps".equals(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SAFFSTree.SAFFSNode child = pr.tree.getChild(pr.node, name);
|
||||
if (child == null) {
|
||||
child = pr.tree.createFile(pr.node, name);
|
||||
}
|
||||
if (child == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (InputStream is = zf.getInputStream(entry);
|
||||
ParcelFileDescriptor pfd = pr.tree.createFileDescriptor(child, "wt")) {
|
||||
copyStream(new FileOutputStream(pfd.getFileDescriptor()), is);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return false;
|
||||
} catch(IOException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void copyStream(OutputStream out, InputStream in) throws IOException {
|
||||
byte[] buffer = new byte[4096];
|
||||
int sz;
|
||||
while((sz = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, sz);
|
||||
}
|
||||
}
|
||||
}
|
||||
566
backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
Normal file
566
backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
Normal file
@@ -0,0 +1,566 @@
|
||||
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.Insets;
|
||||
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;
|
||||
import android.media.AudioTrack;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
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.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
class CompatHelpers {
|
||||
static class HideSystemStatusBar {
|
||||
|
||||
public static void hide(final Window window) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
HideSystemStatusBarR.hide(window);
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||
HideSystemStatusBarKitKat.hide(window);
|
||||
} else {
|
||||
HideSystemStatusBarJellyBean.hide(window);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.JELLY_BEAN)
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class HideSystemStatusBarJellyBean {
|
||||
public static void hide(final Window window) {
|
||||
View view = window.getDecorView();
|
||||
view.setSystemUiVisibility(
|
||||
view.getSystemUiVisibility() |
|
||||
View.SYSTEM_UI_FLAG_LOW_PROFILE);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.KITKAT)
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class HideSystemStatusBarKitKat {
|
||||
public static void hide(final Window window) {
|
||||
View view = window.getDecorView();
|
||||
view.setSystemUiVisibility(
|
||||
(view.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_IMMERSIVE) |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.R)
|
||||
private static class HideSystemStatusBarR {
|
||||
public static void hide(final Window window) {
|
||||
WindowInsetsController insetsController = window.getInsetsController();
|
||||
insetsController.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
|
||||
insetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class SystemInsets {
|
||||
public interface SystemInsetsListener {
|
||||
void systemInsetsUpdated(int[] gestureInsets, int[] systemInsets, int[] cutoutInsets);
|
||||
}
|
||||
|
||||
public static void registerSystemInsetsListener(View v, SystemInsetsListener l) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
v.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListenerR(l));
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
v.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListenerQ(l));
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
||||
v.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListenerP(l));
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
v.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListenerLollipop(l));
|
||||
} else {
|
||||
// Not available
|
||||
int[] gestureInsets = new int[] { 0, 0, 0, 0 };
|
||||
int[] systemInsets = new int[] { 0, 0, 0, 0 };
|
||||
int[] cutoutInsets = new int[] { 0, 0, 0, 0 };
|
||||
l.systemInsetsUpdated(gestureInsets, systemInsets, cutoutInsets);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP)
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class OnApplyWindowInsetsListenerLollipop implements View.OnApplyWindowInsetsListener {
|
||||
final private SystemInsetsListener l;
|
||||
|
||||
public OnApplyWindowInsetsListenerLollipop(SystemInsetsListener l) {
|
||||
this.l = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
// No system gestures inset before Android Q
|
||||
int[] gestureInsets = new int[] {
|
||||
insets.getStableInsetLeft(),
|
||||
insets.getStableInsetTop(),
|
||||
insets.getStableInsetRight(),
|
||||
insets.getStableInsetBottom()
|
||||
};
|
||||
int[] systemInsets = new int[] {
|
||||
insets.getSystemWindowInsetLeft(),
|
||||
insets.getSystemWindowInsetTop(),
|
||||
insets.getSystemWindowInsetRight(),
|
||||
insets.getSystemWindowInsetBottom()
|
||||
};
|
||||
// No cutouts before Android P
|
||||
int[] cutoutInsets = new int[] { 0, 0, 0, 0 };
|
||||
l.systemInsetsUpdated(gestureInsets, systemInsets, cutoutInsets);
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.P)
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class OnApplyWindowInsetsListenerP implements View.OnApplyWindowInsetsListener {
|
||||
final private SystemInsetsListener l;
|
||||
|
||||
public OnApplyWindowInsetsListenerP(SystemInsetsListener l) {
|
||||
this.l = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
// No system gestures inset before Android Q
|
||||
int[] gestureInsets = new int[] {
|
||||
insets.getStableInsetLeft(),
|
||||
insets.getStableInsetTop(),
|
||||
insets.getStableInsetRight(),
|
||||
insets.getStableInsetBottom()
|
||||
};
|
||||
int[] systemInsets = new int[] {
|
||||
insets.getSystemWindowInsetLeft(),
|
||||
insets.getSystemWindowInsetTop(),
|
||||
insets.getSystemWindowInsetRight(),
|
||||
insets.getSystemWindowInsetBottom()
|
||||
};
|
||||
int[] cutoutInsets;
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout == null) {
|
||||
cutoutInsets = new int[] { 0, 0, 0, 0 };
|
||||
} else {
|
||||
cutoutInsets = new int[] {
|
||||
cutout.getSafeInsetLeft(),
|
||||
cutout.getSafeInsetTop(),
|
||||
cutout.getSafeInsetRight(),
|
||||
cutout.getSafeInsetBottom()
|
||||
};
|
||||
}
|
||||
l.systemInsetsUpdated(gestureInsets, systemInsets, cutoutInsets);
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.Q)
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class OnApplyWindowInsetsListenerQ implements View.OnApplyWindowInsetsListener {
|
||||
final private SystemInsetsListener l;
|
||||
|
||||
public OnApplyWindowInsetsListenerQ(SystemInsetsListener l) {
|
||||
this.l = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
Insets insetsStruct = insets.getSystemGestureInsets();
|
||||
int[] gestureInsets = new int[] {
|
||||
insetsStruct.left,
|
||||
insetsStruct.top,
|
||||
insetsStruct.right,
|
||||
insetsStruct.bottom,
|
||||
};
|
||||
insetsStruct = insets.getSystemWindowInsets();
|
||||
int[] systemInsets = new int[] {
|
||||
insetsStruct.left,
|
||||
insetsStruct.top,
|
||||
insetsStruct.right,
|
||||
insetsStruct.bottom,
|
||||
};
|
||||
int[] cutoutInsets;
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout == null) {
|
||||
cutoutInsets = new int[] { 0, 0, 0, 0 };
|
||||
} else {
|
||||
cutoutInsets = new int[] {
|
||||
cutout.getSafeInsetLeft(),
|
||||
cutout.getSafeInsetTop(),
|
||||
cutout.getSafeInsetRight(),
|
||||
cutout.getSafeInsetBottom()
|
||||
};
|
||||
}
|
||||
l.systemInsetsUpdated(gestureInsets, systemInsets, cutoutInsets);
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.R)
|
||||
private static class OnApplyWindowInsetsListenerR implements View.OnApplyWindowInsetsListener {
|
||||
final private SystemInsetsListener l;
|
||||
|
||||
public OnApplyWindowInsetsListenerR(SystemInsetsListener l) {
|
||||
this.l = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
Insets insetsStruct = insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemGestures());
|
||||
int[] gestureInsets = new int[] {
|
||||
insetsStruct.left,
|
||||
insetsStruct.top,
|
||||
insetsStruct.right,
|
||||
insetsStruct.bottom,
|
||||
};
|
||||
insetsStruct = insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars());
|
||||
int[] systemInsets = new int[] {
|
||||
insetsStruct.left,
|
||||
insetsStruct.top,
|
||||
insetsStruct.right,
|
||||
insetsStruct.bottom,
|
||||
};
|
||||
int[] cutoutInsets;
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout == null) {
|
||||
cutoutInsets = new int[] { 0, 0, 0, 0 };
|
||||
} else {
|
||||
cutoutInsets = new int[] {
|
||||
cutout.getSafeInsetLeft(),
|
||||
cutout.getSafeInsetTop(),
|
||||
cutout.getSafeInsetRight(),
|
||||
cutout.getSafeInsetBottom()
|
||||
};
|
||||
}
|
||||
l.systemInsetsUpdated(gestureInsets, systemInsets, cutoutInsets);
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class AudioTrackCompat {
|
||||
public static class AudioTrackCompatReturn {
|
||||
public AudioTrack audioTrack;
|
||||
public int bufferSize;
|
||||
}
|
||||
|
||||
public static AudioTrackCompatReturn make(int sample_rate, int buffer_size) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
return AudioTrackCompatM.make(sample_rate, buffer_size);
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
return AudioTrackCompatLollipop.make(sample_rate, buffer_size);
|
||||
} else {
|
||||
return AudioTrackCompatOld.make(sample_rate, buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Support for Android KitKat or lower
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class AudioTrackCompatOld {
|
||||
public static AudioTrackCompatReturn make(int sample_rate, int buffer_size) {
|
||||
AudioTrackCompatReturn ret = new AudioTrackCompatReturn();
|
||||
ret.audioTrack = new AudioTrack(
|
||||
AudioManager.STREAM_MUSIC,
|
||||
sample_rate,
|
||||
AudioFormat.CHANNEL_OUT_STEREO,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
buffer_size,
|
||||
AudioTrack.MODE_STREAM);
|
||||
ret.bufferSize = buffer_size;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP)
|
||||
private static class AudioTrackCompatLollipop {
|
||||
public static AudioTrackCompatReturn make(int sample_rate, int buffer_size) {
|
||||
AudioTrackCompatReturn ret = new AudioTrackCompatReturn();
|
||||
ret.audioTrack = new AudioTrack(
|
||||
new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_GAME)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build(),
|
||||
new AudioFormat.Builder()
|
||||
.setSampleRate(sample_rate)
|
||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO).build(),
|
||||
buffer_size,
|
||||
AudioTrack.MODE_STREAM,
|
||||
AudioManager.AUDIO_SESSION_ID_GENERATE);
|
||||
ret.bufferSize = buffer_size;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.M)
|
||||
private static class AudioTrackCompatM {
|
||||
public static AudioTrackCompatReturn make(int sample_rate, int buffer_size) {
|
||||
AudioTrackCompatReturn ret = new AudioTrackCompatReturn();
|
||||
ret.audioTrack = new AudioTrack(
|
||||
new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_GAME)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build(),
|
||||
new AudioFormat.Builder()
|
||||
.setSampleRate(sample_rate)
|
||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO).build(),
|
||||
buffer_size,
|
||||
AudioTrack.MODE_STREAM,
|
||||
AudioManager.AUDIO_SESSION_ID_GENERATE);
|
||||
// Keep track of the actual obtained audio buffer size, if supported.
|
||||
// We just requested 16 bit PCM stereo pcm so there are 4 bytes per frame.
|
||||
ret.bufferSize = ret.audioTrack.getBufferSizeInFrames() * 4;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class AccessibilityEventConstructor {
|
||||
public static AccessibilityEvent make(int eventType) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
return AccessibilityEventConstructorR.make(eventType);
|
||||
} else {
|
||||
return AccessibilityEventConstructorOld.make(eventType);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class AccessibilityEventConstructorOld {
|
||||
public static AccessibilityEvent make(int eventType) {
|
||||
return AccessibilityEvent.obtain(eventType);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.R)
|
||||
private static class AccessibilityEventConstructorR {
|
||||
public static AccessibilityEvent make(int eventType) {
|
||||
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.N_MR1) {
|
||||
return ShortcutCreatorN_MR1.createShortcutResultIntent(context, id, intent, label, icon, fallbackIconId);
|
||||
} else {
|
||||
return ShortcutCreatorOld.createShortcutResultIntent(context, id, intent, label, icon, fallbackIconId);
|
||||
}
|
||||
}
|
||||
|
||||
public static void pushDynamicShortcut(@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.R) {
|
||||
ShortcutCreatorR.pushDynamicShortcut(context, id, intent, label, icon, fallbackIconId);
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
|
||||
ShortcutCreatorN_MR1.pushDynamicShortcut(context, id, intent, label, icon, fallbackIconId);
|
||||
}
|
||||
// No support for older versions
|
||||
}
|
||||
|
||||
@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.N_MR1)
|
||||
private static class ShortcutCreatorN_MR1 {
|
||||
public static ShortcutInfo createShortcutInfo(Context context, String id, @NonNull Intent intent, @NonNull String label, @Nullable Icon icon) {
|
||||
ShortcutInfo.Builder builder = new ShortcutInfo.Builder(context, id);
|
||||
builder.setIntent(intent);
|
||||
builder.setShortLabel(label);
|
||||
builder.setIcon(icon);
|
||||
HashSet<String> categories = new HashSet<>(1);
|
||||
categories.add("actions.intent.START_GAME_EVENT");
|
||||
builder.setCategories(categories);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static Intent createShortcutResultIntent(Context context, String id, @NonNull Intent intent, @NonNull String label, @Nullable Drawable icon, @DrawableRes int fallbackIconId) {
|
||||
Bitmap bm;
|
||||
Icon ic;
|
||||
if (icon != null) {
|
||||
bm = drawableToBitmap(icon);
|
||||
ic = Icon.createWithBitmap(bm);
|
||||
} else {
|
||||
icon = DrawableCompat.getDrawable(context, fallbackIconId);
|
||||
Objects.requireNonNull(icon);
|
||||
bm = drawableToBitmap(icon);
|
||||
ic = Icon.createWithResource(context, fallbackIconId);
|
||||
}
|
||||
ShortcutInfo si = createShortcutInfo(context, id, intent, label, ic);
|
||||
|
||||
Intent result = null;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
final ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
|
||||
result = shortcutManager.createShortcutResultIntent(si);
|
||||
}
|
||||
if (result == null) {
|
||||
result = new Intent();
|
||||
}
|
||||
ShortcutCreatorOld.addToIntent(result, intent, label, bm);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void pushDynamicShortcut(@NonNull Context context, String id, @NonNull Intent intent, @NonNull String label, @Nullable Drawable icon, @DrawableRes int fallbackIconId) {
|
||||
Icon ic;
|
||||
if (icon != null) {
|
||||
ic = Icon.createWithBitmap(drawableToBitmap(icon));
|
||||
} else {
|
||||
ic = Icon.createWithResource(context, fallbackIconId);
|
||||
}
|
||||
ShortcutInfo si = createShortcutInfo(context, id, intent, label, ic);
|
||||
|
||||
final ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
|
||||
if (shortcutManager.isRateLimitingActive()) {
|
||||
return;
|
||||
}
|
||||
List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts();
|
||||
// Sort shortcuts by rank, timestamp and id
|
||||
Collections.sort(shortcuts, new Comparator<ShortcutInfo>() {
|
||||
@Override
|
||||
public int compare(ShortcutInfo a, ShortcutInfo b) {
|
||||
int ret = Integer.compare(a.getRank(), b.getRank());
|
||||
if (ret != 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = Long.compare(a.getLastChangedTimestamp(), b.getLastChangedTimestamp());
|
||||
if (ret != 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
return a.getId().compareTo(b.getId());
|
||||
}
|
||||
});
|
||||
|
||||
// In old Android versions, only 4 shortcuts are displayed but 5 maximum are supported
|
||||
// Problem: the last one added is not displayed... so stick to 4
|
||||
int maxSize = Math.min(shortcutManager.getMaxShortcutCountPerActivity(), 4);
|
||||
if (shortcuts.size() >= maxSize) {
|
||||
int toRemove = shortcuts.size() - maxSize + 1;
|
||||
ArrayList<String> toRemoveList = new ArrayList<>(toRemove);
|
||||
// Remove the lowest rank, oldest shortcut if we need it
|
||||
for(ShortcutInfo oldSi : shortcuts) {
|
||||
if (oldSi.getId().equals(id)) {
|
||||
// We will update it: no need to make space
|
||||
toRemoveList.clear();
|
||||
break;
|
||||
}
|
||||
if (toRemove > 0) {
|
||||
toRemoveList.add(oldSi.getId());
|
||||
toRemove -= 1;
|
||||
}
|
||||
}
|
||||
shortcutManager.removeDynamicShortcuts(toRemoveList);
|
||||
}
|
||||
shortcuts = new ArrayList<>(1);
|
||||
shortcuts.add(si);
|
||||
shortcutManager.addDynamicShortcuts(shortcuts);
|
||||
shortcutManager.reportShortcutUsed(id);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.R)
|
||||
private static class ShortcutCreatorR {
|
||||
public static void pushDynamicShortcut(@NonNull Context context, String id, @NonNull Intent intent, @NonNull String label, @Nullable Drawable icon, @DrawableRes int fallbackIconId) {
|
||||
Icon ic;
|
||||
if (icon != null) {
|
||||
ic = Icon.createWithBitmap(drawableToBitmap(icon));
|
||||
} else {
|
||||
ic = Icon.createWithResource(context, fallbackIconId);
|
||||
}
|
||||
ShortcutInfo si = ShortcutCreatorN_MR1.createShortcutInfo(context, id, intent, label, ic);
|
||||
|
||||
final ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
|
||||
shortcutManager.pushDynamicShortcut(si);
|
||||
// pushDynamicShortcut already reports usage
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,860 @@
|
||||
/*
|
||||
* Copyright (C) 2008-2009 Google Inc.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.annotation.XmlRes;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
|
||||
/**
|
||||
* Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
|
||||
* consists of rows of keys.
|
||||
* <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
|
||||
* <pre>
|
||||
* <Keyboard
|
||||
* android:keyWidth="%10p"
|
||||
* android:keyHeight="50px"
|
||||
* android:horizontalGap="2px"
|
||||
* android:verticalGap="2px" >
|
||||
* <Row android:keyWidth="32px" >
|
||||
* <Key android:keyLabel="A" />
|
||||
* ...
|
||||
* </Row>
|
||||
* ...
|
||||
* </Keyboard>
|
||||
* </pre>
|
||||
*/
|
||||
public class CustomKeyboard {
|
||||
|
||||
static final String LOG_TAG = "CustomKeyboard";
|
||||
|
||||
// Keyboard XML Tags
|
||||
private static final String TAG_KEYBOARD = "CustomKeyboard";
|
||||
private static final String TAG_ROW = "CustomRow";
|
||||
private static final String TAG_KEY = "CustomKey";
|
||||
|
||||
public static final int EDGE_LEFT = 0x01;
|
||||
public static final int EDGE_RIGHT = 0x02;
|
||||
public static final int EDGE_TOP = 0x04;
|
||||
public static final int EDGE_BOTTOM = 0x08;
|
||||
|
||||
public static final int KEYCODE_SHIFT = -1;
|
||||
public static final int KEYCODE_MODE_CHANGE = -2;
|
||||
public static final int KEYCODE_CANCEL = -3;
|
||||
public static final int KEYCODE_DONE = -4;
|
||||
public static final int KEYCODE_DELETE = -5;
|
||||
public static final int KEYCODE_ALT = -6;
|
||||
|
||||
/** Keyboard label **/
|
||||
private CharSequence mLabel;
|
||||
|
||||
/** Horizontal gap default for all rows */
|
||||
private int mDefaultHorizontalGap;
|
||||
|
||||
/** Default key width */
|
||||
private int mDefaultWidth;
|
||||
|
||||
/** Default key height */
|
||||
private int mDefaultHeight;
|
||||
|
||||
/** Default gap between rows */
|
||||
private int mDefaultVerticalGap;
|
||||
|
||||
/** Is the keyboard in the shifted state */
|
||||
private boolean mShifted;
|
||||
|
||||
/** Key instance for the shift key, if present */
|
||||
private CustomKey[] mShiftKeys = { null, null };
|
||||
|
||||
/** Key index for the shift key, if present */
|
||||
private int[] mShiftKeyIndices = {-1, -1};
|
||||
|
||||
/** Current key width, while loading the keyboard */
|
||||
private int mKeyWidth;
|
||||
|
||||
/** Current key height, while loading the keyboard */
|
||||
private int mKeyHeight;
|
||||
|
||||
/** Total height of the keyboard, including the padding and keys */
|
||||
// @UnsupportedAppUsage
|
||||
private int mTotalHeight;
|
||||
|
||||
/**
|
||||
* Total width of the keyboard, including left side gaps and keys, but not any gaps on the
|
||||
* right side.
|
||||
*/
|
||||
// @UnsupportedAppUsage
|
||||
private int mTotalWidth;
|
||||
|
||||
/** List of keys in this keyboard */
|
||||
private List<CustomKey> mKeys;
|
||||
|
||||
/** List of modifier keys such as Shift & Alt, if any */
|
||||
// @UnsupportedAppUsage
|
||||
private List<CustomKey> mModifierKeys;
|
||||
|
||||
/** Width of the screen available to fit the keyboard */
|
||||
private int mDisplayWidth;
|
||||
|
||||
/** Height of the screen */
|
||||
private int mDisplayHeight;
|
||||
|
||||
/** Keyboard mode, or zero, if none. */
|
||||
private int mKeyboardMode;
|
||||
|
||||
// Variables for pre-computing nearest keys.
|
||||
|
||||
private static final int GRID_WIDTH = 10;
|
||||
private static final int GRID_HEIGHT = 5;
|
||||
private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
|
||||
private int mCellWidth;
|
||||
private int mCellHeight;
|
||||
private int[][] mGridNeighbors;
|
||||
private int mProximityThreshold;
|
||||
/** Number of key widths from current touch point to search for nearest keys. */
|
||||
private static float SEARCH_DISTANCE = 1.8f;
|
||||
|
||||
private ArrayList<CustomRow> rows = new ArrayList<CustomRow>();
|
||||
|
||||
/**
|
||||
* Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
|
||||
* Some of the key size defaults can be overridden per row from what the {@link CustomKeyboard}
|
||||
* defines.
|
||||
*/
|
||||
public static class CustomRow {
|
||||
/** Default width of a key in this row. */
|
||||
public int defaultWidth;
|
||||
/** Default height of a key in this row. */
|
||||
public int defaultHeight;
|
||||
/** Default horizontal gap between keys in this row. */
|
||||
public int defaultHorizontalGap;
|
||||
/** Vertical gap following this row. */
|
||||
public int verticalGap;
|
||||
|
||||
ArrayList<CustomKey> mKeys = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Edge flags for this row of keys. Possible values that can be assigned are
|
||||
* {@link CustomKeyboard#EDGE_TOP EDGE_TOP} and {@link CustomKeyboard#EDGE_BOTTOM EDGE_BOTTOM}
|
||||
*/
|
||||
public int rowEdgeFlags;
|
||||
|
||||
/** The keyboard mode for this row */
|
||||
public int mode;
|
||||
|
||||
private CustomKeyboard parent;
|
||||
|
||||
public CustomRow(CustomKeyboard parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public CustomRow(Resources res, CustomKeyboard parent, XmlResourceParser parser) {
|
||||
this.parent = parent;
|
||||
TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.CustomKeyboard);
|
||||
defaultWidth = getDimensionOrFraction(a, R.styleable.CustomKeyboard_keyWidth, parent.mDisplayWidth, parent.mDefaultWidth);
|
||||
defaultHeight = getDimensionOrFraction(a, R.styleable.CustomKeyboard_keyHeight, parent.mDisplayHeight, parent.mDefaultHeight);
|
||||
defaultHorizontalGap = getDimensionOrFraction(a, R.styleable.CustomKeyboard_horizontalGap, parent.mDisplayWidth, parent.mDefaultHorizontalGap);
|
||||
verticalGap = getDimensionOrFraction(a, R.styleable.CustomKeyboard_verticalGap, parent.mDisplayHeight, parent.mDefaultVerticalGap);
|
||||
a.recycle();
|
||||
|
||||
a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.CustomKeyboard_CustomRow);
|
||||
rowEdgeFlags = a.getInt(R.styleable.CustomKeyboard_CustomRow_rowEdgeFlags, 0);
|
||||
mode = a.getResourceId(R.styleable.CustomKeyboard_CustomRow_keyboardMode, 0);
|
||||
a.recycle();
|
||||
}
|
||||
} // end of: static class CustomRow
|
||||
|
||||
/**
|
||||
* Class for describing the position and characteristics of a single key in the keyboard.
|
||||
*
|
||||
*/
|
||||
public static class CustomKey {
|
||||
/**
|
||||
* All the key codes (unicode or custom code) that this key could generate, zero'th
|
||||
* being the most important.
|
||||
*/
|
||||
public int[] codes;
|
||||
|
||||
/** Label to display */
|
||||
public CharSequence label;
|
||||
|
||||
/** Icon to display instead of a label. Icon takes precedence over a label */
|
||||
public Drawable icon;
|
||||
/** Preview version of the icon, for the preview popup */
|
||||
public Drawable iconPreview;
|
||||
/** Width of the key, not including the gap */
|
||||
public int width;
|
||||
/** Height of the key, not including the gap */
|
||||
public int height;
|
||||
/** The horizontal gap before this key */
|
||||
public int gap;
|
||||
/** Whether this key is sticky, i.e., a toggle key */
|
||||
public boolean sticky;
|
||||
/** X coordinate of the key in the keyboard layout */
|
||||
public int x;
|
||||
/** Y coordinate of the key in the keyboard layout */
|
||||
public int y;
|
||||
/** The current pressed state of this key */
|
||||
public boolean pressed;
|
||||
/** If this is a sticky key, is it on? */
|
||||
public boolean on;
|
||||
/** Text to output when pressed. This can be multiple characters, like ".com" */
|
||||
public CharSequence text;
|
||||
/** Popup characters */
|
||||
public CharSequence popupCharacters;
|
||||
|
||||
/**
|
||||
* Flags that specify the anchoring to edges of the keyboard for detecting touch events
|
||||
* that are just out of the boundary of the key. This is a bit mask of
|
||||
* {@link CustomKeyboard#EDGE_LEFT}, {@link CustomKeyboard#EDGE_RIGHT}, {@link CustomKeyboard#EDGE_TOP} and
|
||||
* {@link CustomKeyboard#EDGE_BOTTOM}.
|
||||
*/
|
||||
public int edgeFlags;
|
||||
/** Whether this is a modifier key, such as Shift or Alt */
|
||||
public boolean modifier;
|
||||
/** The keyboard that this key belongs to */
|
||||
private CustomKeyboard keyboard;
|
||||
/**
|
||||
* If this key pops up a mini keyboard, this is the resource id for the XML layout for that
|
||||
* keyboard.
|
||||
*/
|
||||
public int popupResId;
|
||||
/** Whether this key repeats itself when held down */
|
||||
public boolean repeatable;
|
||||
|
||||
|
||||
private final static int[] KEY_STATE_NORMAL_ON = {
|
||||
android.R.attr.state_checkable,
|
||||
android.R.attr.state_checked
|
||||
};
|
||||
|
||||
private final static int[] KEY_STATE_PRESSED_ON = {
|
||||
android.R.attr.state_pressed,
|
||||
android.R.attr.state_checkable,
|
||||
android.R.attr.state_checked
|
||||
};
|
||||
|
||||
private final static int[] KEY_STATE_NORMAL_OFF = {
|
||||
android.R.attr.state_checkable
|
||||
};
|
||||
|
||||
private final static int[] KEY_STATE_PRESSED_OFF = {
|
||||
android.R.attr.state_pressed,
|
||||
android.R.attr.state_checkable
|
||||
};
|
||||
|
||||
private final static int[] KEY_STATE_NORMAL = { };
|
||||
|
||||
private final static int[] KEY_STATE_PRESSED = {
|
||||
android.R.attr.state_pressed
|
||||
};
|
||||
|
||||
/** Create an empty key with no attributes. */
|
||||
public CustomKey(CustomRow parent) {
|
||||
keyboard = parent.parent;
|
||||
height = parent.defaultHeight;
|
||||
width = parent.defaultWidth;
|
||||
gap = parent.defaultHorizontalGap;
|
||||
edgeFlags = parent.rowEdgeFlags;
|
||||
}
|
||||
|
||||
/** Create a key with the given top-left coordinate and extract its attributes from
|
||||
* the XML parser.
|
||||
* @param res resources associated with the caller's context
|
||||
* @param parent the row that this key belongs to. The row must already be attached to
|
||||
* a {@link CustomKeyboard}.
|
||||
* @param x the x coordinate of the top-left
|
||||
* @param y the y coordinate of the top-left
|
||||
* @param parser the XML parser containing the attributes for this key
|
||||
*/
|
||||
public CustomKey(Resources res, CustomRow parent, int x, int y, XmlResourceParser parser) {
|
||||
this(parent);
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
// obtainAttributes (AttributeSet set, int[] attrs)
|
||||
// Retrieve a set of basic attribute values from an AttributeSet, not performing styling of them using a theme and/or style resources.
|
||||
TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.CustomKeyboard);
|
||||
|
||||
width = getDimensionOrFraction(a, R.styleable.CustomKeyboard_keyWidth, keyboard.mDisplayWidth, parent.defaultWidth);
|
||||
height = getDimensionOrFraction(a, R.styleable.CustomKeyboard_keyHeight, keyboard.mDisplayHeight, parent.defaultHeight);
|
||||
gap = getDimensionOrFraction(a, R.styleable.CustomKeyboard_horizontalGap, keyboard.mDisplayWidth, parent.defaultHorizontalGap);
|
||||
|
||||
// Log.d(LOG_TAG, "from CustomKeyboard: wid: " +width + " heigh: " + height + " gap: " + gap );
|
||||
|
||||
a.recycle();
|
||||
a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.CustomKeyboard_CustomKey);
|
||||
this.x += gap;
|
||||
TypedValue codesValue = new TypedValue();
|
||||
a.getValue(R.styleable.CustomKeyboard_CustomKey_codes, codesValue);
|
||||
if (codesValue.type == TypedValue.TYPE_INT_DEC || codesValue.type == TypedValue.TYPE_INT_HEX) {
|
||||
// Log.d(LOG_TAG, "Key codes is INT or HEX");
|
||||
codes = new int[] { codesValue.data };
|
||||
} else if (codesValue.type == TypedValue.TYPE_STRING) {
|
||||
// Log.d(LOG_TAG, "Key codes is String");
|
||||
codes = parseCSV(codesValue.string.toString());
|
||||
}
|
||||
|
||||
iconPreview = a.getDrawable(R.styleable.CustomKeyboard_CustomKey_iconPreview);
|
||||
if (iconPreview != null) {
|
||||
iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(), iconPreview.getIntrinsicHeight());
|
||||
}
|
||||
popupCharacters = a.getText(R.styleable.CustomKeyboard_CustomKey_popupCharacters);
|
||||
popupResId = a.getResourceId(R.styleable.CustomKeyboard_CustomKey_popupKeyboard, 0);
|
||||
repeatable = a.getBoolean(R.styleable.CustomKeyboard_CustomKey_isRepeatable, false);
|
||||
modifier = a.getBoolean(R.styleable.CustomKeyboard_CustomKey_isModifier, false);
|
||||
sticky = a.getBoolean(R.styleable.CustomKeyboard_CustomKey_isSticky, false);
|
||||
edgeFlags = a.getInt(R.styleable.CustomKeyboard_CustomKey_keyEdgeFlags, 0);
|
||||
edgeFlags |= parent.rowEdgeFlags;
|
||||
|
||||
icon = a.getDrawable(R.styleable.CustomKeyboard_CustomKey_keyIcon);
|
||||
if (icon != null) {
|
||||
icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
|
||||
}
|
||||
label = a.getText(R.styleable.CustomKeyboard_CustomKey_keyLabel);
|
||||
// Log.d(LOG_TAG, "Key label is " + label);
|
||||
|
||||
text = a.getText(R.styleable.CustomKeyboard_CustomKey_keyOutputText);
|
||||
|
||||
if (codes == null && !TextUtils.isEmpty(label)) {
|
||||
codes = new int[] { label.charAt(0) };
|
||||
}
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the key that it has been pressed, in case it needs to change its appearance or
|
||||
* state.
|
||||
* @see #onReleased(boolean)
|
||||
*/
|
||||
public void onPressed() {
|
||||
pressed = !pressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the pressed state of the key.
|
||||
*
|
||||
* <p>Toggled state of the key will be flipped when all the following conditions are
|
||||
* fulfilled:</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>This is a sticky key, that is, {@link #sticky} is {@code true}.
|
||||
* <li>The parameter {@code inside} is {@code true}.
|
||||
* <li>{@link android.os.Build.VERSION#SDK_INT} is greater than
|
||||
* {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
|
||||
* </ul>
|
||||
*
|
||||
* @param inside whether the finger was released inside the key. Works only on Android M and
|
||||
* later. See the method document for details.
|
||||
* @see #onPressed()
|
||||
*/
|
||||
public void onReleased(boolean inside) {
|
||||
pressed = !pressed;
|
||||
if (sticky && inside) {
|
||||
on = !on;
|
||||
}
|
||||
}
|
||||
|
||||
int[] parseCSV(String value) {
|
||||
int count = 0;
|
||||
int lastIndex = 0;
|
||||
if (value.length() > 0) {
|
||||
count++;
|
||||
while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
int[] values = new int[count];
|
||||
count = 0;
|
||||
StringTokenizer st = new StringTokenizer(value, ",");
|
||||
while (st.hasMoreTokens()) {
|
||||
try {
|
||||
values[count++] = Integer.parseInt(st.nextToken());
|
||||
} catch (NumberFormatException nfe) {
|
||||
Log.e(LOG_TAG, "Error parsing keycodes " + value);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a point falls inside this key.
|
||||
* @param x the x-coordinate of the point
|
||||
* @param y the y-coordinate of the point
|
||||
* @return whether or not the point falls inside the key. If the key is attached to an edge,
|
||||
* it will assume that all points between the key and the edge are considered to be inside
|
||||
* the key.
|
||||
*/
|
||||
public boolean isInside(int x, int y) {
|
||||
boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0;
|
||||
boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0;
|
||||
boolean topEdge = (edgeFlags & EDGE_TOP) > 0;
|
||||
boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0;
|
||||
if ((x >= this.x || (leftEdge && x <= this.x + this.width))
|
||||
&& (x < this.x + this.width || (rightEdge && x >= this.x))
|
||||
&& (y >= this.y || (topEdge && y <= this.y + this.height))
|
||||
&& (y < this.y + this.height || (bottomEdge && y >= this.y))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the square of the distance between the center of the key and the given point.
|
||||
* @param x the x-coordinate of the point
|
||||
* @param y the y-coordinate of the point
|
||||
* @return the square of the distance of the point from the center of the key
|
||||
*/
|
||||
public int squaredDistanceFrom(int x, int y) {
|
||||
int xDist = this.x + width / 2 - x;
|
||||
int yDist = this.y + height / 2 - y;
|
||||
return xDist * xDist + yDist * yDist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the drawable state for the key, based on the current state and type of the key.
|
||||
* @return the drawable state of the key.
|
||||
* @see android.graphics.drawable.StateListDrawable#setState(int[])
|
||||
*/
|
||||
public int[] getCurrentDrawableState() {
|
||||
int[] states = KEY_STATE_NORMAL;
|
||||
|
||||
if (on) {
|
||||
if (pressed) {
|
||||
states = KEY_STATE_PRESSED_ON;
|
||||
} else {
|
||||
states = KEY_STATE_NORMAL_ON;
|
||||
}
|
||||
} else {
|
||||
if (sticky) {
|
||||
if (pressed) {
|
||||
states = KEY_STATE_PRESSED_OFF;
|
||||
} else {
|
||||
states = KEY_STATE_NORMAL_OFF;
|
||||
}
|
||||
} else {
|
||||
if (pressed) {
|
||||
states = KEY_STATE_PRESSED;
|
||||
}
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
} // end of: static class CustomKey
|
||||
|
||||
/**
|
||||
* Creates a keyboard from the given xml key layout file.
|
||||
* @param context the application or service context
|
||||
* @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
|
||||
*/
|
||||
public CustomKeyboard(Context context, int xmlLayoutResId) {
|
||||
this(context, xmlLayoutResId, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a keyboard from the given xml key layout file. Weeds out rows
|
||||
* that have a keyboard mode defined but don't match the specified mode.
|
||||
* @param context the application or service context
|
||||
* @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
|
||||
* @param modeId keyboard mode identifier
|
||||
* @param width sets width of keyboard
|
||||
* @param height sets height of keyboard
|
||||
*/
|
||||
public CustomKeyboard(Context context, @XmlRes int xmlLayoutResId, int modeId, int width, int height) {
|
||||
mDisplayWidth = width;
|
||||
mDisplayHeight = height;
|
||||
|
||||
mDefaultHorizontalGap = 0;
|
||||
mDefaultWidth = mDisplayWidth / 10;
|
||||
mDefaultVerticalGap = 0;
|
||||
mDefaultHeight = mDefaultWidth;
|
||||
mKeys = new ArrayList<CustomKey>();
|
||||
mModifierKeys = new ArrayList<CustomKey>();
|
||||
mKeyboardMode = modeId;
|
||||
loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a keyboard from the given xml key layout file. Weeds out rows
|
||||
* that have a keyboard mode defined but don't match the specified mode.
|
||||
* @param context the application or service context
|
||||
* @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
|
||||
* @param modeId keyboard mode identifier
|
||||
*/
|
||||
public CustomKeyboard(Context context, @XmlRes int xmlLayoutResId, int modeId) {
|
||||
DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
||||
mDisplayWidth = dm.widthPixels;
|
||||
mDisplayHeight = dm.heightPixels;
|
||||
// Log.v(LOG_TAG, "keyboard's display metrics:" + dm);
|
||||
|
||||
mDefaultHorizontalGap = 0;
|
||||
mDefaultWidth = mDisplayWidth / 10;
|
||||
mDefaultVerticalGap = 0;
|
||||
mDefaultHeight = mDefaultWidth;
|
||||
mKeys = new ArrayList<CustomKey>();
|
||||
mModifierKeys = new ArrayList<CustomKey>();
|
||||
mKeyboardMode = modeId;
|
||||
// Log.v(LOG_TAG, "Resource ID is " + xmlLayoutResId + " and parser is null?" + ((context.getResources().getXml(xmlLayoutResId) == null) ? "yes" : "no"));
|
||||
loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Creates a blank keyboard from the given resource file and populates it with the specified
|
||||
* characters in left-to-right, top-to-bottom fashion, using the specified number of columns.
|
||||
* </p>
|
||||
* <p>If the specified number of columns is -1, then the keyboard will fit as many keys as
|
||||
* possible in each row.</p>
|
||||
* @param context the application or service context
|
||||
* @param layoutTemplateResId the layout template file, containing no keys.
|
||||
* @param characters the list of characters to display on the keyboard. One key will be created
|
||||
* for each character.
|
||||
* @param columns the number of columns of keys to display. If this number is greater than the
|
||||
* number of keys that can fit in a row, it will be ignored. If this number is -1, the
|
||||
* keyboard will fit as many keys as possible in each row.
|
||||
*/
|
||||
public CustomKeyboard(Context context, int layoutTemplateResId, CharSequence characters, int columns, int horizontalPadding) {
|
||||
this(context, layoutTemplateResId);
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int column = 0;
|
||||
mTotalWidth = 0;
|
||||
|
||||
CustomRow row = new CustomRow(this);
|
||||
row.defaultHeight = mDefaultHeight;
|
||||
row.defaultWidth = mDefaultWidth;
|
||||
row.defaultHorizontalGap = mDefaultHorizontalGap;
|
||||
row.verticalGap = mDefaultVerticalGap;
|
||||
row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM;
|
||||
final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
|
||||
for (int i = 0; i < characters.length(); i++) {
|
||||
char c = characters.charAt(i);
|
||||
if (column >= maxColumns || x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
|
||||
x = 0;
|
||||
y += mDefaultVerticalGap + mDefaultHeight;
|
||||
column = 0;
|
||||
}
|
||||
final CustomKey key = new CustomKey(row);
|
||||
key.x = x;
|
||||
key.y = y;
|
||||
key.label = String.valueOf(c);
|
||||
key.codes = new int[] { c };
|
||||
column++;
|
||||
x += key.width + key.gap;
|
||||
mKeys.add(key);
|
||||
row.mKeys.add(key);
|
||||
if (x > mTotalWidth) {
|
||||
mTotalWidth = x;
|
||||
}
|
||||
}
|
||||
mTotalHeight = y + mDefaultHeight;
|
||||
rows.add(row);
|
||||
}
|
||||
|
||||
// @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
||||
final void resize(int newWidth, int newHeight) {
|
||||
int numRows = rows.size();
|
||||
for (int rowIndex = 0; rowIndex < numRows; ++rowIndex) {
|
||||
CustomRow row = rows.get(rowIndex);
|
||||
int numKeys = row.mKeys.size();
|
||||
int totalGap = 0;
|
||||
int totalWidth = 0;
|
||||
for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
|
||||
CustomKey key = row.mKeys.get(keyIndex);
|
||||
if (keyIndex > 0) {
|
||||
totalGap += key.gap;
|
||||
}
|
||||
totalWidth += key.width;
|
||||
}
|
||||
if (totalGap + totalWidth > newWidth) {
|
||||
int x = 0;
|
||||
float scaleFactor = (float)(newWidth - totalGap) / totalWidth;
|
||||
for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
|
||||
CustomKey key = row.mKeys.get(keyIndex);
|
||||
key.width *= scaleFactor;
|
||||
key.x = x;
|
||||
x += key.width + key.gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
mTotalWidth = newWidth;
|
||||
// TODO: This does not adjust the vertical placement according to the new size.
|
||||
// The main problem in the previous code was horizontal placement/size, but we should
|
||||
// also recalculate the vertical sizes/positions when we get this resize call.
|
||||
}
|
||||
|
||||
public List<CustomKey> getKeys() {
|
||||
return mKeys;
|
||||
}
|
||||
|
||||
public List<CustomKey> getModifierKeys() {
|
||||
return mModifierKeys;
|
||||
}
|
||||
|
||||
protected int getHorizontalGap() {
|
||||
return mDefaultHorizontalGap;
|
||||
}
|
||||
|
||||
protected void setHorizontalGap(int gap) {
|
||||
mDefaultHorizontalGap = gap;
|
||||
}
|
||||
|
||||
protected int getVerticalGap() {
|
||||
return mDefaultVerticalGap;
|
||||
}
|
||||
|
||||
protected void setVerticalGap(int gap) {
|
||||
mDefaultVerticalGap = gap;
|
||||
}
|
||||
|
||||
protected int getKeyHeight() {
|
||||
return mDefaultHeight;
|
||||
}
|
||||
|
||||
protected void setKeyHeight(int height) {
|
||||
mDefaultHeight = height;
|
||||
}
|
||||
|
||||
protected int getKeyWidth() {
|
||||
return mDefaultWidth;
|
||||
}
|
||||
|
||||
protected void setKeyWidth(int width) {
|
||||
mDefaultWidth = width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total height of the keyboard
|
||||
* @return the total height of the keyboard
|
||||
*/
|
||||
public int getHeight() {
|
||||
return mTotalHeight;
|
||||
}
|
||||
|
||||
public int getMinWidth() {
|
||||
return mTotalWidth;
|
||||
}
|
||||
|
||||
public boolean setShifted(boolean shiftState) {
|
||||
for (CustomKey shiftKey : mShiftKeys) {
|
||||
if (shiftKey != null) {
|
||||
shiftKey.on = shiftState;
|
||||
}
|
||||
}
|
||||
if (mShifted != shiftState) {
|
||||
mShifted = shiftState;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isShifted() {
|
||||
return mShifted;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public int[] getShiftKeyIndices() {
|
||||
return mShiftKeyIndices;
|
||||
}
|
||||
|
||||
public int getShiftKeyIndex() {
|
||||
return mShiftKeyIndices[0];
|
||||
}
|
||||
|
||||
private void computeNearestNeighbors() {
|
||||
// Round-up so we don't have any pixels outside the grid
|
||||
mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
|
||||
mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
|
||||
mGridNeighbors = new int[GRID_SIZE][];
|
||||
int[] indices = new int[mKeys.size()];
|
||||
final int gridWidth = GRID_WIDTH * mCellWidth;
|
||||
final int gridHeight = GRID_HEIGHT * mCellHeight;
|
||||
for (int x = 0; x < gridWidth; x += mCellWidth) {
|
||||
for (int y = 0; y < gridHeight; y += mCellHeight) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < mKeys.size(); i++) {
|
||||
final CustomKey key = mKeys.get(i);
|
||||
if (key.squaredDistanceFrom(x, y) < mProximityThreshold
|
||||
|| key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold
|
||||
|| key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1) < mProximityThreshold
|
||||
|| key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold) {
|
||||
indices[count++] = i;
|
||||
}
|
||||
}
|
||||
int [] cell = new int[count];
|
||||
System.arraycopy(indices, 0, cell, 0, count);
|
||||
mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the indices of the keys that are closest to the given point.
|
||||
* @param x the x-coordinate of the point
|
||||
* @param y the y-coordinate of the point
|
||||
* @return the array of integer indices for the nearest keys to the given point. If the given
|
||||
* point is out of range, then an array of size zero is returned.
|
||||
*/
|
||||
public int[] getNearestKeys(int x, int y) {
|
||||
if (mGridNeighbors == null) computeNearestNeighbors();
|
||||
if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
|
||||
int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
|
||||
if (index < GRID_SIZE) {
|
||||
return mGridNeighbors[index];
|
||||
}
|
||||
}
|
||||
return new int[0];
|
||||
}
|
||||
|
||||
protected CustomRow createRowFromXml(Resources res, XmlResourceParser parser) {
|
||||
return new CustomRow(res, this, parser);
|
||||
}
|
||||
|
||||
protected CustomKey createKeyFromXml(Resources res, CustomRow parent, int x, int y, XmlResourceParser parser) {
|
||||
return new CustomKey(res, parent, x, y, parser);
|
||||
}
|
||||
|
||||
private void loadKeyboard(Context context, XmlResourceParser parser) {
|
||||
boolean inKey = false;
|
||||
boolean inRow = false;
|
||||
boolean leftMostKey = false;
|
||||
int row = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
CustomKey key = null;
|
||||
CustomRow currentRow = null;
|
||||
Resources res = context.getResources();
|
||||
boolean skipRow = false;
|
||||
|
||||
try {
|
||||
int event;
|
||||
while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
|
||||
if (event == XmlResourceParser.START_TAG) {
|
||||
String tag = parser.getName();
|
||||
if (TAG_ROW.equals(tag)) {
|
||||
// Log.d(LOG_TAG, "TAG ROW");
|
||||
inRow = true;
|
||||
x = 0;
|
||||
currentRow = createRowFromXml(res, parser);
|
||||
rows.add(currentRow);
|
||||
skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode;
|
||||
if (skipRow) {
|
||||
skipToEndOfRow(parser);
|
||||
inRow = false;
|
||||
}
|
||||
} else if (TAG_KEY.equals(tag)) {
|
||||
// Log.d(LOG_TAG, "TAG KEY");
|
||||
inKey = true;
|
||||
key = createKeyFromXml(res, currentRow, x, y, parser);
|
||||
mKeys.add(key);
|
||||
if (key.codes != null) {
|
||||
if (key.codes[0] == KEYCODE_SHIFT) {
|
||||
// Find available shift key slot and put this shift key in it
|
||||
for (int i = 0; i < mShiftKeys.length; i++) {
|
||||
if (mShiftKeys[i] == null) {
|
||||
mShiftKeys[i] = key;
|
||||
mShiftKeyIndices[i] = mKeys.size()-1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
mModifierKeys.add(key);
|
||||
} else if (key.codes[0] == KEYCODE_ALT) {
|
||||
mModifierKeys.add(key);
|
||||
}
|
||||
currentRow.mKeys.add(key);
|
||||
}
|
||||
} else if (TAG_KEYBOARD.equals(tag)) {
|
||||
parseKeyboardAttributes(res, parser);
|
||||
}
|
||||
} else if (event == XmlResourceParser.END_TAG) {
|
||||
if (inKey) {
|
||||
inKey = false;
|
||||
x += key.gap + key.width;
|
||||
if (x > mTotalWidth) {
|
||||
mTotalWidth = x;
|
||||
}
|
||||
} else if (inRow) {
|
||||
inRow = false;
|
||||
y += currentRow.verticalGap;
|
||||
y += currentRow.defaultHeight;
|
||||
row++;
|
||||
} else {
|
||||
// TODO: error or extend?
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Parse error:" + e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
mTotalHeight = y - mDefaultVerticalGap;
|
||||
}
|
||||
|
||||
private void skipToEndOfRow(XmlResourceParser parser) throws XmlPullParserException, IOException {
|
||||
int event;
|
||||
while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
|
||||
if (event == XmlResourceParser.END_TAG && parser.getName().equals(TAG_ROW)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) {
|
||||
TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.CustomKeyboard);
|
||||
|
||||
mDefaultWidth = getDimensionOrFraction(a, R.styleable.CustomKeyboard_keyWidth, mDisplayWidth, mDisplayWidth / 10);
|
||||
mDefaultHeight = getDimensionOrFraction(a, R.styleable.CustomKeyboard_keyHeight, mDisplayHeight, 50);
|
||||
mDefaultHorizontalGap = getDimensionOrFraction(a, R.styleable.CustomKeyboard_horizontalGap, mDisplayWidth, 0);
|
||||
mDefaultVerticalGap = getDimensionOrFraction(a, R.styleable.CustomKeyboard_verticalGap, mDisplayHeight, 0);
|
||||
mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE);
|
||||
mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) {
|
||||
TypedValue value = a.peekValue(index);
|
||||
if (value == null)
|
||||
return defValue;
|
||||
|
||||
if (value.type == TypedValue.TYPE_DIMENSION) {
|
||||
return a.getDimensionPixelOffset(index, defValue);
|
||||
} else if (value.type == TypedValue.TYPE_FRACTION) {
|
||||
// Round it to avoid values like 47.9999 from getting truncated
|
||||
return Math.round(a.getFraction(index, base, base, defValue));
|
||||
}
|
||||
return defValue;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
public class EditableAccommodatingLatinIMETypeNullIssues extends SpannableStringBuilder {
|
||||
EditableAccommodatingLatinIMETypeNullIssues(CharSequence source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
//This character must be ignored by your onKey() code.
|
||||
public static final CharSequence ONE_UNPROCESSED_CHARACTER = "\\";
|
||||
|
||||
@Override
|
||||
public SpannableStringBuilder replace(final int spannableStringStart, final int spannableStringEnd, CharSequence replacementSequence, int replacementStart, int replacementEnd) {
|
||||
if (replacementEnd > replacementStart) {
|
||||
//In this case, there is something in the replacementSequence that the IME
|
||||
// is attempting to replace part of the editable with.
|
||||
//We don't really care about whatever might already be in the editable;
|
||||
// we only care about making sure that SOMETHING ends up in it,
|
||||
// so that the backspace key will continue to work.
|
||||
// So, start by zeroing out whatever is there to begin with.
|
||||
super.replace(0, length(), "", 0, 0);
|
||||
|
||||
//We DO care about preserving the new stuff that is replacing the stuff in the
|
||||
// editable, because this stuff might be sent to us as a keyDown event. So, we
|
||||
// insert the new stuff (typically, a single character) into the now-empty editable,
|
||||
// and return the result to the caller.
|
||||
return super.replace(0, 0, replacementSequence, replacementStart, replacementEnd);
|
||||
} else if (spannableStringEnd > spannableStringStart) {
|
||||
//In this case, there is NOTHING in the replacementSequence, and something is
|
||||
// being replaced in the editable.
|
||||
// This is characteristic of a DELETION.
|
||||
// So, start by zeroing out whatever is being replaced in the editable.
|
||||
super.replace(0, length(), "", 0, 0);
|
||||
|
||||
//And now, we will place our ONE_UNPROCESSED_CHARACTER into the editable buffer, and return it.
|
||||
return super.replace(0, 0, ONE_UNPROCESSED_CHARACTER, 0, 1);
|
||||
}
|
||||
|
||||
// In this case, NOTHING is being replaced in the editable. This code assumes that there
|
||||
// is already something there. This assumption is probably OK because in our
|
||||
// InputConnectionAccommodatingLatinIMETypeNullIssues.getEditable() method
|
||||
// we PLACE a ONE_UNPROCESSED_CHARACTER into the newly-created buffer. So if there
|
||||
// is nothing replacing the identified part
|
||||
// of the editable, and no part of the editable that is being replaced, then we just
|
||||
// leave whatever is in the editable ALONE,
|
||||
// and we can be confident that there will be SOMETHING there. This call to super.replace()
|
||||
// in that case will be a no-op, except
|
||||
// for the value it returns.
|
||||
return super.replace(spannableStringStart, spannableStringEnd,
|
||||
replacementSequence, replacementStart, replacementEnd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.Selection;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.PointerIcon;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
public class EditableSurfaceView extends SurfaceView {
|
||||
final Context _context;
|
||||
final boolean _allowHideSystemMousePointer;
|
||||
private boolean _mouseIsInCapturedState;
|
||||
|
||||
public EditableSurfaceView(Context context) {
|
||||
|
||||
super(context);
|
||||
_context = context;
|
||||
_mouseIsInCapturedState = false;
|
||||
_allowHideSystemMousePointer = true;
|
||||
}
|
||||
|
||||
public EditableSurfaceView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
_context = context;
|
||||
_mouseIsInCapturedState = false;
|
||||
_allowHideSystemMousePointer = true;
|
||||
}
|
||||
|
||||
public EditableSurfaceView(Context context,
|
||||
AttributeSet attrs,
|
||||
int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
_context = context;
|
||||
_mouseIsInCapturedState = false;
|
||||
_allowHideSystemMousePointer = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCheckIsTextEditor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private class MyInputConnection extends BaseInputConnection {
|
||||
// The false second argument puts the BaseInputConnection into "dummy" mode, which is also required in order for the raw key events to be sent to your view.
|
||||
// In the BaseInputConnection code, you can find several comments such as the following: "only if dummy mode, a key event is sent for the new text and the current editable buffer cleared."
|
||||
// Reference: https://stackoverflow.com/a/7386854
|
||||
public MyInputConnection() {
|
||||
super(EditableSurfaceView.this, false);
|
||||
}
|
||||
|
||||
// Bug fixes for backspace behavior in TYPE_NULL input type
|
||||
// ref: https://stackoverflow.com/a/19980975
|
||||
// This holds the Editable text buffer that the LatinIME mistakenly *thinks*
|
||||
// that it is editing, even though the views that employ this class are
|
||||
// completely driven by key events.
|
||||
Editable _myEditable = null;
|
||||
|
||||
//This method is called by the IME whenever the view that returned an
|
||||
// instance of this class to the IME from its onCreateInputConnection()
|
||||
// gains focus.
|
||||
@Override
|
||||
public Editable getEditable() {
|
||||
// Some versions of the Google Keyboard (LatinIME) were delivered with a
|
||||
// bug that causes KEYCODE_DEL to no longer be generated once the number
|
||||
// of KEYCODE_DEL taps equals the number of other characters that have
|
||||
// been typed. This bug was reported here as issue 62306.
|
||||
//
|
||||
// As of this writing (1/7/2014), it is fixed in the AOSP code, but that
|
||||
// fix has not yet been released. Even when it is released, there will
|
||||
// be many devices having versions of the Google Keyboard that include the bug
|
||||
// in the wild for the indefinite future. Therefore, a workaround is required.
|
||||
//
|
||||
// This is a workaround for that bug which just jams a single garbage character
|
||||
// into the internal buffer that the keyboard THINKS it is editing even
|
||||
// though we have specified TYPE_NULL which *should* cause LatinIME to
|
||||
// generate key events regardless of what is in that buffer. We have other
|
||||
// code that attempts to ensure as the user edites that there is always
|
||||
// one character remaining.
|
||||
//
|
||||
// The problem arises because when this unseen buffer becomes empty, the IME
|
||||
// thinks that there is nothing left to delete, and therefore stops
|
||||
// generating KEYCODE_DEL events, even though the app may still be very
|
||||
// interested in receiving them.
|
||||
//
|
||||
// So, for example, if the user taps in ABCDE and then positions the
|
||||
// (app-based) cursor to the left of A and taps the backspace key three
|
||||
// times without any evident effect on the letters (because the app's own
|
||||
// UI code knows that there are no letters to the left of the
|
||||
// app-implemented cursor), and then moves the cursor to the right of the
|
||||
// E and hits backspace five times, then, after E and D have been deleted,
|
||||
// no more KEYCODE_DEL events will be generated by the IME because the
|
||||
// unseen buffer will have become empty from five letter key taps followed
|
||||
// by five backspace key taps (as the IME is unaware of the app-based cursor
|
||||
// movements performed by the user).
|
||||
//
|
||||
// In other words, if your app is processing KEYDOWN events itself, and
|
||||
// maintaining its own cursor and so on, and not telling the IME anything
|
||||
// about the user's cursor position, this buggy processing of the hidden
|
||||
// buffer will stop KEYCODE_DEL events when your app actually needs them -
|
||||
// in whatever Android releases incorporate this LatinIME bug.
|
||||
//
|
||||
// By creating this garbage characters in the Editable that is initially
|
||||
// returned to the IME here, we make the IME think that it still has
|
||||
// something to delete, which causes it to keep generating KEYCODE_DEL
|
||||
// events in response to backspace key presses.
|
||||
//
|
||||
// A specific keyboard version that I tested this on which HAS this
|
||||
// problem but does NOT have the "KEYCODE_DEL completely gone" (issue 42904)
|
||||
// problem that is addressed by the deleteSurroundingText() override below
|
||||
// (the two problems are not both present in a single version) is
|
||||
// 2.0.19123.914326a, tested running on a Nexus7 2012 tablet.
|
||||
// There may be other versions that have issue 62306.
|
||||
//
|
||||
// A specific keyboard version that I tested this on which does NOT have
|
||||
// this problem but DOES have the "KEYCODE_DEL completely gone" (issue
|
||||
// 42904) problem that is addressed by the deleteSurroundingText()
|
||||
// override below is 1.0.1800.776638, tested running on the Nexus10
|
||||
// tablet. There may be other versions that also have issue 42904.
|
||||
//
|
||||
// The bug that this addresses was first introduced as of AOSP commit tag
|
||||
// 4.4_r0.9, and the next RELEASED Android version after that was
|
||||
// android-4.4_r1, which is the first release of Android 4.4. So, 4.4 will
|
||||
// be the first Android version that would have included, in the original
|
||||
// RELEASED version, a Google Keyboard for which this bug was present.
|
||||
//
|
||||
// Note that this bug was introduced exactly at the point that the OTHER bug
|
||||
// (the one that is addressed in deleteSurroundingText(), below) was first
|
||||
// FIXED.
|
||||
//
|
||||
// Despite the fact that the above are the RELEASES associated with the bug,
|
||||
// the fact is that any 4.x Android release could have been upgraded by the
|
||||
// user to a later version of Google Keyboard than was present when the
|
||||
// release was originally installed to the device. I have checked the
|
||||
// www.archive.org snapshots of the Google Keyboard listing page on the Google
|
||||
// Play store, and all released updates listed there (which go back to early
|
||||
// June of 2013) required Android 4.0 and up, so we can be pretty sure that
|
||||
// this bug is not present in any version earlier than 4.0 (ICS), which means
|
||||
// that we can limit this fix to API level 14 and up. And once the LatinIME
|
||||
// problem is fixed, we can limit the scope of this workaround to end as of
|
||||
// the last release that included the problem, since we can assume that
|
||||
// users will not upgrade Google Keyboard to an EARLIER version than was
|
||||
// originally included in their Android release.
|
||||
//
|
||||
// The bug that this addresses was FIXED but NOT RELEASED as of this AOSP
|
||||
// commit:
|
||||
//https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+
|
||||
// /b41bea65502ce7339665859d3c2c81b4a29194e4/java/src/com/android
|
||||
// /inputmethod/latin/LatinIME.java
|
||||
// so it can be assumed to affect all of KitKat released thus far
|
||||
// (up to 4.4.2), and could even affect beyond KitKat, although I fully
|
||||
// expect it to be incorporated into the next release *after* API level 19.
|
||||
//
|
||||
// When it IS released, this method should be changed to limit it to no
|
||||
// higher than API level 19 (assuming that the fix is released before API
|
||||
// level 20), just in order to limit the scope of this fix, since poking
|
||||
// 1024 characters into the Editable object returned here is of course a
|
||||
// kluge. But right now the safest thing is just to not have an upper limit
|
||||
// on the application of this kluge, since the fix for the problem it
|
||||
// addresses has not yet been released (as of 1/7/2014).
|
||||
if(Build.VERSION.SDK_INT >= 14) {
|
||||
if (_myEditable == null) {
|
||||
_myEditable = new EditableAccommodatingLatinIMETypeNullIssues(EditableAccommodatingLatinIMETypeNullIssues.ONE_UNPROCESSED_CHARACTER);
|
||||
Selection.setSelection(_myEditable, 1);
|
||||
} else {
|
||||
int _myEditableLength = _myEditable.length();
|
||||
if (_myEditableLength == 0) {
|
||||
// I actually HAVE seen this be zero on the Nexus 10 with the keyboard
|
||||
// that came with Android 4.4.2
|
||||
// On the Nexus 10 4.4.2 if I tapped away from the view and then back to it, the
|
||||
// _myEditable would come back as null and I would create a new one. This is also
|
||||
// what happens on other devices (e.g., the Nexus 6 with 4.4.2,
|
||||
// which has a slightly later version of the Google Keyboard). But for the
|
||||
// Nexus 10 4.4.2, the keyboard had a strange behavior
|
||||
// when I tapped on the rack, and then tapped Done on the keyboard to close it,
|
||||
// and then tapped on the rack AGAIN. In THAT situation,
|
||||
// the _myEditable would NOT be set to NULL but its LENGTH would be ZERO. So, I
|
||||
// just append to it in that situation.
|
||||
_myEditable.append(EditableAccommodatingLatinIMETypeNullIssues.ONE_UNPROCESSED_CHARACTER);
|
||||
Selection.setSelection(_myEditable, 1);
|
||||
}
|
||||
}
|
||||
return _myEditable;
|
||||
}
|
||||
else {
|
||||
//Default behavior for keyboards that do not require any fix
|
||||
return super.getEditable();
|
||||
}
|
||||
}
|
||||
|
||||
// This method is called INSTEAD of generating a KEYCODE_DEL event, by
|
||||
// versions of Latin IME that have the bug described in Issue 42904.
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||
// If targetSdkVersion is set to anything AT or ABOVE API level 16
|
||||
// then for the GOOGLE KEYBOARD versions DELIVERED
|
||||
// with Android 4.1.x, 4.2.x or 4.3.x, NO KEYCODE_DEL EVENTS WILL BE
|
||||
// GENERATED BY THE GOOGLE KEYBOARD (LatinIME) EVEN when TYPE_NULL
|
||||
// is being returned as the InputType by your view from its
|
||||
// onCreateInputMethod() override, due to a BUG in THOSE VERSIONS.
|
||||
//
|
||||
// When TYPE_NULL is specified (as this entire class assumes is being done
|
||||
// by the views that use it, what WILL be generated INSTEAD of a KEYCODE_DEL
|
||||
// is a deleteSurroundingText(1,0) call. So, by overriding this
|
||||
// deleteSurroundingText() method, we can fire the KEYDOWN/KEYUP events
|
||||
// ourselves for KEYCODE_DEL. This provides a workaround for the bug.
|
||||
//
|
||||
// The specific AOSP RELEASES involved are 4.1.1_r1 (the very first 4.1
|
||||
// release) through 4.4_r0.8 (the release just prior to Android 4.4).
|
||||
// This means that all of KitKat should not have the bug and will not
|
||||
// need this workaround.
|
||||
//
|
||||
// Although 4.0.x (ICS) did not have this bug, it was possible to install
|
||||
// later versions of the keyboard as an app on anything running 4.0 and up,
|
||||
// so those versions are also potentially affected.
|
||||
//
|
||||
// The first version of separately-installable Google Keyboard shown on the
|
||||
// Google Play store site by www.archive.org is Version 1.0.1869.683049,
|
||||
// on June 6, 2013, and that version (and probably other, later ones)
|
||||
// already had this bug.
|
||||
//
|
||||
//Since this required at least 4.0 to install, I believe that the bug will
|
||||
// not be present on devices running versions of Android earlier than 4.0.
|
||||
//
|
||||
// AND, it should not be present on versions of Android at 4.4 and higher,
|
||||
// since users will not "upgrade" to a version of Google Keyboard that
|
||||
// is LOWER than the one they got installed with their version of Android
|
||||
// in the first place, and the bug will have been fixed as of the 4.4 release.
|
||||
//
|
||||
// The above scope of the bug is reflected in the test below, which limits
|
||||
// the application of the workaround to Android versions between 4.0.x and 4.3.x.
|
||||
//
|
||||
// UPDATE: A popular third party keyboard was found that exhibits this same issue. It
|
||||
// was not fixed at the same time as the Google Play keyboard, and so the bug in that case
|
||||
// is still in place beyond API LEVEL 19. So, even though the Google Keyboard fixed this
|
||||
// as of level 19, we cannot take out the fix based on that version number. And so I've
|
||||
// removed the test for an upper limit on the version; the fix will remain in place ad
|
||||
// infinitum - but only when TYPE_NULL is used, so it *should* be harmless even when
|
||||
// the keyboard does not have the problem...
|
||||
if ((Build.VERSION.SDK_INT >= 14) // && (Build.VERSION.SDK_INT < 19)
|
||||
&& (beforeLength == 1 && afterLength == 0)) {
|
||||
//Send Backspace key down and up events to replace the ones omitted
|
||||
// by the LatinIME keyboard.
|
||||
return super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
|
||||
&& super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
|
||||
} else {
|
||||
//Really, I can't see how this would be invoked, given that we're using
|
||||
// TYPE_NULL, for non-buggy versions, but in order to limit the impact
|
||||
// of this change as much as possible (i.e., to versions at and above 4.0)
|
||||
// I am using the original behavior here for non-affected versions.
|
||||
return super.deleteSurroundingText(beforeLength, afterLength);
|
||||
}
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public boolean performEditorAction(int actionCode) {
|
||||
// if (actionCode == EditorInfo.IME_ACTION_DONE) {
|
||||
// InputMethodManager imm = (InputMethodManager)
|
||||
// getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
// imm.hideSoftInputFromWindow(getWindowToken(), 0);
|
||||
// }
|
||||
//
|
||||
// // Sends enter key
|
||||
// return super.performEditorAction(actionCode);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
outAttrs.initialCapsMode = 0;
|
||||
outAttrs.initialSelEnd = outAttrs.initialSelStart = -1;
|
||||
outAttrs.actionLabel = null;
|
||||
|
||||
// Per the documentation for InputType.TYPE_NULL:
|
||||
// "This should be interpreted to mean that the target input connection is not rich,
|
||||
// it can not process and show things like candidate text nor retrieve the current text,
|
||||
// so the input method will need to run in a limited 'generate key events' mode."
|
||||
// Reference: https://stackoverflow.com/a/7386854
|
||||
// We lose auto-complete, but that is ok, because we *really* want direct input key handling
|
||||
outAttrs.inputType = InputType.TYPE_NULL;
|
||||
|
||||
// IME_FLAG_NO_EXTRACT_UI used to specify that the IME does not need to show its extracted text UI. Extract UI means the fullscreen editing mode.
|
||||
// IME_ACTION_NONE Bits of IME_MASK_ACTION: there is no available action.
|
||||
// TODO should we have a IME_ACTION_DONE?
|
||||
outAttrs.imeOptions = (EditorInfo.IME_ACTION_NONE |
|
||||
EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
||||
|
||||
return new MyInputConnection();
|
||||
}
|
||||
|
||||
public void showSystemMouseCursor(boolean show) {
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::showSystemMouseCursor2 " + show);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// Android N (Nougat) is Android 7.0
|
||||
//SurfaceView main_surface = findViewById(R.id.main_surface);
|
||||
int type = show ? PointerIcon.TYPE_ARROW : PointerIcon.TYPE_NULL;
|
||||
// https://stackoverflow.com/a/55482761
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::showSystemMouseCursor3a");
|
||||
setPointerIcon(PointerIcon.getSystemIcon(_context, type));
|
||||
} else {
|
||||
/* Currently hiding the system mouse cursor is only
|
||||
supported on OUYA. If other systems provide similar
|
||||
intents, please add them here as well */
|
||||
Intent intent =
|
||||
new Intent(show?
|
||||
"tv.ouya.controller.action.SHOW_CURSOR" :
|
||||
"tv.ouya.controller.action.HIDE_CURSOR");
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::showSystemMouseCursor3b");
|
||||
_context.sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
||||
// This re-inforces the code for hiding the system mouse.
|
||||
// We already had code for this in ScummVMActivity (see showSystemMouseCursor())
|
||||
// so this might be redundant
|
||||
//
|
||||
// It applies on devices running Android 7 and above
|
||||
// https://stackoverflow.com/a/55482761
|
||||
// https://developer.android.com/reference/android/view/PointerIcon.html
|
||||
//
|
||||
@TargetApi(24)
|
||||
@Override
|
||||
public PointerIcon onResolvePointerIcon(MotionEvent me, int pointerIndex) {
|
||||
if (_allowHideSystemMousePointer) {
|
||||
if (_mouseIsInCapturedState) {
|
||||
return PointerIcon.getSystemIcon(_context, PointerIcon.TYPE_NULL);
|
||||
} else {
|
||||
return PointerIcon.getSystemIcon(_context, PointerIcon.TYPE_ARROW);
|
||||
}
|
||||
} else {
|
||||
return PointerIcon.getSystemIcon(_context, PointerIcon.TYPE_ARROW);
|
||||
}
|
||||
}
|
||||
|
||||
public void captureMouse(final boolean capture) {
|
||||
|
||||
if ((!_mouseIsInCapturedState && ((ScummVMActivity)_context).isKeyboardOverlayShown() && capture)) {
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::returned - keyboard is shown");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((capture && _mouseIsInCapturedState) ||
|
||||
(!capture && ! _mouseIsInCapturedState)) {
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::returned - nothing to do");
|
||||
return;
|
||||
}
|
||||
|
||||
if (capture) {
|
||||
// setFocusableInTouchMode(true);
|
||||
// setFocusable(true);
|
||||
// requestFocus();
|
||||
_mouseIsInCapturedState = true;
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::_mouseIsInCapturedState");
|
||||
} else {
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::no _mouseIsInCapturedState");
|
||||
_mouseIsInCapturedState = false;
|
||||
}
|
||||
|
||||
showSystemMouseCursor(!capture);
|
||||
|
||||
// trying capturing the pointer resulted in firing TrackBallEvent instead of HoverEvent for our SurfaceView
|
||||
// also the behavior was inconsistent when resuming to the app from a pause
|
||||
// if (_allowHideSystemMousePointer) {
|
||||
// // for API 26 and above
|
||||
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
// Log.d(ScummVM.LOG_TAG, "captureMouse::CODES 0");
|
||||
// if (capture) {
|
||||
// postDelayed( new Runnable() {
|
||||
// public void run()
|
||||
// {
|
||||
// Log.v(ScummVM.LOG_TAG, "captureMouse::requestPointerCapture() delayed");
|
||||
//// if (!hasPointerCapture()) {
|
||||
// requestPointerCapture();
|
||||
//// showSystemMouseCursor(!capture);
|
||||
//// }
|
||||
// }
|
||||
// }, 50 );
|
||||
// } else {
|
||||
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
// postDelayed(new Runnable() {
|
||||
// public void run() {
|
||||
// Log.v(ScummVM.LOG_TAG, "captureMouse::releasePointerCapture()");
|
||||
//// if (hasPointerCapture()) {
|
||||
// releasePointerCapture();
|
||||
//// showSystemMouseCursor(!capture);
|
||||
//// }
|
||||
// }
|
||||
// }, 50);
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// Log.d(ScummVM.LOG_TAG, "captureMouse::NO CODES 0 showSystemMouseCursor " + !capture);
|
||||
// showSystemMouseCursor(!capture);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* Contains helper methods to get list of available media
|
||||
*/
|
||||
public class ExternalStorage {
|
||||
public static final String SD_CARD = "sdCard";
|
||||
public static final String EXTERNAL_SD_CARD = "externalSdCard";
|
||||
|
||||
// User data is an system wide folder (typically, but maybe not always, "/data/") where apps can store data in their own subfolder.
|
||||
// While this folder does exists in newer Android OS versions it is most likely not *directly* usable.
|
||||
public static final String DATA_DIRECTORY = "User data (System Wide)";
|
||||
|
||||
// Internal App Data folder is a folder that is guaranteed to always be available for access.
|
||||
// Only the app (here ScummVM) can write and read under this folder.
|
||||
// It is used to store configuration file(s), log file(s), the default saved games folder, the default icons folder, and distribution data files.
|
||||
// The folder's contents are kept kept upon upgrading to a compatible newer version of the app.
|
||||
// It is wiped when downgrading, uninstalling or explicitly cleaning the application's data from the Android System Apps menu options.
|
||||
// The storage for this folder is assigned from internal device storage (ie. not external physical SD Card).
|
||||
public static final String DATA_DIRECTORY_INT = "ScummVM data (Internal)";
|
||||
|
||||
// External App Data folder is a folder that is NOT guaranteed to always be available for access.
|
||||
// Only the app (here ScummVM) can write under this folder, but other apps have read access to the folder's contents.
|
||||
// The folder's contents are kept upon upgrading to a compatible newer version of the app.
|
||||
// It is wiped when downgrading, uninstalling or explicitly cleaning the application's data from the Android System Apps menu options.
|
||||
public static final String DATA_DIRECTORY_EXT = "ScummVM data (External)";
|
||||
|
||||
// Find candidate removable sd card paths
|
||||
// Code reference: https://stackoverflow.com/a/54411385
|
||||
private static final String ANDROID_DIR = File.separator + "Android";
|
||||
|
||||
private static String ancestor(File dir) {
|
||||
// getExternalFilesDir() and getExternalStorageDirectory()
|
||||
// may return something app-specific like:
|
||||
// /storage/sdcard1/Android/data/com.mybackuparchives.android/files
|
||||
// so we want the great-great-grandparent folder.
|
||||
if (dir == null) {
|
||||
return null;
|
||||
} else {
|
||||
String path = dir.getAbsolutePath();
|
||||
int i = path.indexOf(ANDROID_DIR);
|
||||
if (i == -1) {
|
||||
return path;
|
||||
} else {
|
||||
return path.substring(0, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern that SD card device should match
|
||||
private static final Pattern
|
||||
devicePattern = Pattern.compile("/dev/(block/.*vold.*|fuse)|/mnt/.*");
|
||||
// Pattern that SD card mount path should match
|
||||
private static final Pattern
|
||||
pathPattern = Pattern.compile("/(mnt|storage|external_sd|extsd|_ExternalSD|Removable|.*MicroSD).*", Pattern.CASE_INSENSITIVE);
|
||||
// Pattern that the mount path should not match.
|
||||
//' emulated' indicates an internal storage location, so skip it.
|
||||
// 'asec' is an encrypted package file, decrypted and mounted as a directory.
|
||||
private static final Pattern
|
||||
pathAntiPattern = Pattern.compile(".*(/secure|/asec|/emulated).*");
|
||||
// These are expected fs types, including vfat. tmpfs is not OK.
|
||||
// fuse can be removable SD card (as on Moto E or Asus ZenPad), or can be internal (Huawei G610).
|
||||
private static final Pattern
|
||||
fsTypePattern = Pattern.compile(".*(fat|msdos|ntfs|ext[34]|fuse|sdcard|esdfs).*");
|
||||
|
||||
/** Common paths for microSD card. **/
|
||||
private static final String[] commonPaths = {
|
||||
// Some of these taken from
|
||||
// https://stackoverflow.com/questions/13976982/removable-storage-external-sdcard-path-by-manufacturers
|
||||
// These are roughly in order such that the earlier ones, if they exist, are more sure
|
||||
// to be removable storage than the later ones.
|
||||
"/mnt/Removable/MicroSD",
|
||||
"/storage/removable/" + SD_CARD + "1", // !< Sony Xperia Z1
|
||||
"/Removable/MicroSD", // Asus ZenPad C
|
||||
"/removable/microsd",
|
||||
"/external_sd", // Samsung
|
||||
"/_ExternalSD", // some LGs
|
||||
"/storage/extSdCard", // later Samsung
|
||||
"/storage/extsdcard", // Main filesystem is case-sensitive; FAT isn't.
|
||||
"/mnt/extsd", // some Chinese tablets, e.g. Zeki
|
||||
"/storage/" + SD_CARD + "1", // If this exists it's more likely than sdcard0 to be removable.
|
||||
"/mnt/extSdCard",
|
||||
"/mnt/" + SD_CARD + "/external_sd",
|
||||
"/mnt/external_sd",
|
||||
"/storage/external_SD",
|
||||
"/storage/ext_sd", // HTC One Max
|
||||
"/mnt/" + SD_CARD + "/_ExternalSD",
|
||||
"/mnt/" + SD_CARD + "-ext",
|
||||
|
||||
"/" + SD_CARD + "2", // HTC One M8s
|
||||
"/" + SD_CARD + "1", // Sony Xperia Z
|
||||
"/mnt/media_rw/" + SD_CARD + "1", // 4.4.2 on CyanogenMod S3
|
||||
"/mnt/" + SD_CARD, // This can be built-in storage (non-removable).
|
||||
"/" + SD_CARD,
|
||||
"/storage/" + SD_CARD +"0",
|
||||
"/emmc",
|
||||
"/mnt/emmc",
|
||||
"/" + SD_CARD + "/sd",
|
||||
"/mnt/" + SD_CARD + "/bpemmctest",
|
||||
"/mnt/external1",
|
||||
"/data/sdext4",
|
||||
"/data/sdext3",
|
||||
"/data/sdext2",
|
||||
"/data/sdext",
|
||||
"/storage/microsd" //ASUS ZenFone 2
|
||||
|
||||
// If we ever decide to support USB OTG storage, the following paths could be helpful:
|
||||
// An LG Nexus 5 apparently uses usb://1002/UsbStorage/ as a URI to access an SD
|
||||
// card over OTG cable. Other models, like Galaxy S5, use /storage/UsbDriveA
|
||||
// "/mnt/usb_storage",
|
||||
// "/mnt/UsbDriveA",
|
||||
// "/mnt/UsbDriveB",
|
||||
};
|
||||
|
||||
/** Find path to removable SD card. */
|
||||
public static LinkedHashSet<File> findSdCardPath() {
|
||||
String[] mountFields;
|
||||
BufferedReader bufferedReader = null;
|
||||
String lineRead;
|
||||
|
||||
// Possible SD card paths
|
||||
LinkedHashSet<File> candidatePaths = new LinkedHashSet<>();
|
||||
|
||||
// Build a list of candidate paths, roughly in order of preference. That way if
|
||||
// we can't definitively detect removable storage, we at least can pick a more likely
|
||||
// candidate.
|
||||
|
||||
// Could do: use getExternalStorageState(File path), with and without an argument, when
|
||||
// available. With an argument is available since API level 21.
|
||||
// This may not be necessary, since we also check whether a directory exists and has contents,
|
||||
// which would fail if the external storage state is neither MOUNTED nor MOUNTED_READ_ONLY.
|
||||
|
||||
// I moved hard-coded paths toward the end, but we need to make sure we put the ones in
|
||||
// backwards order that are returned by the OS. And make sure the iterators respect
|
||||
// the order!
|
||||
// This is because when multiple "external" storage paths are returned, it's always (in
|
||||
// experience, but not guaranteed by documentation) with internal/emulated storage
|
||||
// first, removable storage second.
|
||||
|
||||
// Add value of environment variables as candidates, if set:
|
||||
// EXTERNAL_STORAGE, SECONDARY_STORAGE, EXTERNAL_SDCARD_STORAGE
|
||||
// But note they are *not* necessarily *removable* storage! Especially EXTERNAL_STORAGE.
|
||||
// And they are not documented (API) features. Typically useful only for old versions of Android.
|
||||
|
||||
String val = System.getenv("SECONDARY_STORAGE");
|
||||
if (!TextUtils.isEmpty(val)) {
|
||||
addPath(val, candidatePaths);
|
||||
}
|
||||
|
||||
val = System.getenv("EXTERNAL_SDCARD_STORAGE");
|
||||
if (!TextUtils.isEmpty(val)) {
|
||||
addPath(val, candidatePaths);
|
||||
}
|
||||
|
||||
// Get listing of mounted devices with their properties.
|
||||
ArrayList<File> mountedPaths = new ArrayList<>();
|
||||
try {
|
||||
// Note: Despite restricting some access to /proc (https://stackoverflow.com/a/38728738/423105),
|
||||
// Android 7.0 does *not* block access to /proc/mounts, according to our test on George's Alcatel A30 GSM.
|
||||
bufferedReader = new BufferedReader(new FileReader("/proc/mounts"));
|
||||
|
||||
// Iterate over each line of the mounts listing.
|
||||
while ((lineRead = bufferedReader.readLine()) != null) {
|
||||
// Log.d(ScummVM.LOG_TAG, "\nMounts line: " + lineRead);
|
||||
mountFields = lineRead.split(" ");
|
||||
|
||||
// columns: device, mountpoint, fs type, options... Example:
|
||||
// /dev/block/vold/179:97 /storage/sdcard1 vfat rw,dirsync,nosuid,nodev,noexec,relatime,uid=1000,gid=1015,fmask=0002,dmask=0002,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0
|
||||
String device = mountFields[0], path = mountFields[1], fsType = mountFields[2];
|
||||
|
||||
// The device, path, and fs type must conform to expected patterns.
|
||||
// mtdblock is internal, I'm told.
|
||||
// Check for disqualifying patterns in the path.
|
||||
// If this mounts line fails our tests, skip it.
|
||||
if (!(devicePattern.matcher(device).matches()
|
||||
&& pathPattern.matcher(path).matches()
|
||||
&& fsTypePattern.matcher(fsType).matches())
|
||||
|| device.contains("mtdblock")
|
||||
|| pathAntiPattern.matcher(path).matches()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO maybe: check options to make sure it's mounted RW?
|
||||
// The answer at https://stackoverflow.com/a/13648873/423105 does.
|
||||
// But it hasn't seemed to be necessary so far in my testing.
|
||||
|
||||
// This line met the criteria so far, so add it to candidate list.
|
||||
addPath(path, mountedPaths);
|
||||
}
|
||||
} catch (IOException ignored) { }
|
||||
finally {
|
||||
if (bufferedReader != null) {
|
||||
try {
|
||||
bufferedReader.close();
|
||||
} catch (IOException ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
// Append the paths from mount table to candidate list, in reverse order.
|
||||
if (!mountedPaths.isEmpty()) {
|
||||
// See https://stackoverflow.com/a/5374346/423105 on why the following is necessary.
|
||||
// Basically, .toArray() needs its parameter to know what type of array to return.
|
||||
File[] mountedPathsArray = mountedPaths.toArray(new File[0]);
|
||||
addAncestors(mountedPathsArray, candidatePaths);
|
||||
}
|
||||
|
||||
// Add hard-coded known common paths to candidate list:
|
||||
addStrings(commonPaths, candidatePaths);
|
||||
|
||||
// If the above doesn't work we could try the following other options, but in my experience they
|
||||
// haven't added anything helpful yet.
|
||||
|
||||
// getExternalFilesDir() and getExternalStorageDirectory() typically something app-specific like
|
||||
// /storage/sdcard1/Android/data/com.mybackuparchives.android/files
|
||||
// so we want the great-great-grandparent folder.
|
||||
|
||||
// TODO Note, This method was deprecated in API level 29.
|
||||
// To improve user privacy, direct access to shared/external storage devices is deprecated.
|
||||
// When an app targets Build.VERSION_CODES.Q, the path returned from this method is no longer directly accessible to apps.
|
||||
// Apps can continue to access content stored on shared/external storage by migrating to
|
||||
// alternatives such as Context#getExternalFilesDir(String), MediaStore, or Intent#ACTION_OPEN_DOCUMENT.
|
||||
//
|
||||
// This may be non-removable.
|
||||
Log.d(ScummVM.LOG_TAG, "Environment.getExternalStorageDirectory():");
|
||||
addPath(ancestor(Environment.getExternalStorageDirectory()), candidatePaths);
|
||||
|
||||
// TODO maybe use getExternalStorageState(File path), with and without an argument,
|
||||
// when available. With an argument is available since API level 21.
|
||||
// This may not be necessary, since we also check whether a directory exists,
|
||||
// which would fail if the external storage state is neither MOUNTED nor MOUNTED_READ_ONLY.
|
||||
|
||||
// A "public" external storage directory. But in my experience it doesn't add anything helpful.
|
||||
// Note that you can't pass null, or you'll get an NPE.
|
||||
final File publicDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
|
||||
// Take the parent, because we tend to get a path like /pathTo/sdCard/Music.
|
||||
if (publicDirectory.getParentFile() != null) {
|
||||
addPath(publicDirectory.getParentFile().getAbsolutePath(), candidatePaths);
|
||||
}
|
||||
|
||||
// EXTERNAL_STORAGE: may not be removable.
|
||||
val = System.getenv("EXTERNAL_STORAGE");
|
||||
if (!TextUtils.isEmpty(val)) {
|
||||
addPath(val, candidatePaths);
|
||||
}
|
||||
|
||||
if (candidatePaths.isEmpty()) {
|
||||
Log.w(ScummVM.LOG_TAG, "No removable microSD card found.");
|
||||
return candidatePaths;
|
||||
} else {
|
||||
Log.i(ScummVM.LOG_TAG, "\nFound potential removable storage locations: " + candidatePaths);
|
||||
}
|
||||
|
||||
// Accept or eliminate candidate paths if we can determine whether they're removable storage.
|
||||
// In Lollipop and later, we can check isExternalStorageRemovable() status on each candidate.
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
Iterator<File> itf = candidatePaths.iterator();
|
||||
while (itf.hasNext()) {
|
||||
File dir = itf.next();
|
||||
// handle illegalArgumentException if the path is not a valid storage device.
|
||||
try {
|
||||
if (Environment.isExternalStorageRemovable(dir)) {
|
||||
Log.i(ScummVM.LOG_TAG, dir.getPath() + " is removable external storage");
|
||||
addPath(dir.getAbsolutePath(), candidatePaths);
|
||||
} else if (Environment.isExternalStorageEmulated(dir)) {
|
||||
Log.d(ScummVM.LOG_TAG, "Removing emulated external storage dir " + dir);
|
||||
itf.remove();
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.d(ScummVM.LOG_TAG, "isRemovable(" + dir.getPath() + "): not a valid storage device.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue trying to accept or eliminate candidate paths based on whether they're removable storage.
|
||||
// On pre-Lollipop, we only have singular externalStorage. Check whether it's removable.
|
||||
if (Build.VERSION.SDK_INT >= 9) {
|
||||
File externalStorage = Environment.getExternalStorageDirectory();
|
||||
Log.d(ScummVM.LOG_TAG, String.format(Locale.ROOT, "findSDCardPath: getExternalStorageDirectory = %s", externalStorage.getPath()));
|
||||
if (Environment.isExternalStorageRemovable()) {
|
||||
// Make sure this is a candidate.
|
||||
// TODO: Does this contains() work? Should we be canonicalizing paths before comparing?
|
||||
if (candidatePaths.contains(externalStorage)) {
|
||||
Log.d(ScummVM.LOG_TAG, "Using externalStorage dir " + externalStorage);
|
||||
// return externalStorage;
|
||||
addPath(externalStorage.getAbsolutePath(), candidatePaths);
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= 11 && Environment.isExternalStorageEmulated()) {
|
||||
Log.d(ScummVM.LOG_TAG, "Removing emulated external storage dir " + externalStorage);
|
||||
candidatePaths.remove(externalStorage);
|
||||
}
|
||||
}
|
||||
|
||||
return candidatePaths;
|
||||
}
|
||||
|
||||
|
||||
/** Add each path to the collection. */
|
||||
private static void addStrings(String[] newPaths, LinkedHashSet<File> candidatePaths) {
|
||||
for (String path : newPaths) {
|
||||
addPath(path, candidatePaths);
|
||||
}
|
||||
}
|
||||
|
||||
/** Add ancestor of each File to the collection. */
|
||||
private static void addAncestors(File[] files, LinkedHashSet<File> candidatePaths) {
|
||||
for (int i = files.length - 1; i >= 0; i--) {
|
||||
addPath(ancestor(files[i]), candidatePaths);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new candidate directory path to our list, if it's not obviously wrong.
|
||||
* Supply path as either String or File object.
|
||||
* @param strNew path of directory to add
|
||||
*/
|
||||
private static void addPath(String strNew, Collection<File> paths) {
|
||||
// If one of the arguments is null, fill it in from the other.
|
||||
if (!TextUtils.isEmpty(strNew)) {
|
||||
File fileNew = new File(strNew);
|
||||
|
||||
if (!paths.contains(fileNew) &&
|
||||
// Check for paths known not to be removable SD card.
|
||||
// The antipattern check can be redundant, depending on where this is called from.
|
||||
!pathAntiPattern.matcher(strNew).matches()) {
|
||||
|
||||
// Eliminate candidate if not a directory or not fully accessible.
|
||||
if (fileNew.exists() && fileNew.isDirectory() && fileNew.canExecute()) {
|
||||
Log.d(ScummVM.LOG_TAG, " Adding candidate path " + strNew);
|
||||
paths.add(fileNew);
|
||||
} else {
|
||||
Log.d(ScummVM.LOG_TAG, String.format(Locale.ROOT, " Invalid path %s: exists: %b isDir: %b canExec: %b canRead: %b",
|
||||
strNew, fileNew.exists(), fileNew.isDirectory(), fileNew.canExecute(), fileNew.canRead()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @return True if the external storage is available. False otherwise.
|
||||
*/
|
||||
public static boolean isAvailable() {
|
||||
String state = Environment.getExternalStorageState();
|
||||
return Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
|
||||
}
|
||||
|
||||
public static String getSdCardPath() {
|
||||
return Environment.getExternalStorageDirectory().getPath() + "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the external storage is writable. False otherwise.
|
||||
*/
|
||||
public static boolean isWritable() {
|
||||
String state = Environment.getExternalStorageState();
|
||||
return Environment.MEDIA_MOUNTED.equals(state);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list of locations available. Odd elements are names, even are paths
|
||||
*/
|
||||
public static List<String> getAllStorageLocations(Context ctx) {
|
||||
List<String> map = new ArrayList<>(20);
|
||||
|
||||
List<String> mMounts = new ArrayList<>(10);
|
||||
List<String> mVold = new ArrayList<>(10);
|
||||
mMounts.add("/mnt/sdcard");
|
||||
mVold.add("/mnt/sdcard");
|
||||
|
||||
try {
|
||||
File mountFile = new File("/proc/mounts");
|
||||
if (mountFile.exists()) {
|
||||
Scanner scanner = new Scanner(mountFile);
|
||||
while (scanner.hasNext()) {
|
||||
String line = scanner.nextLine();
|
||||
if (line.startsWith("/dev/block/vold/")) {
|
||||
String[] lineElements = line.split(" ");
|
||||
String element = lineElements[1];
|
||||
|
||||
// don't add the default mount path
|
||||
// it's already in the list.
|
||||
if (!element.equals("/mnt/sdcard"))
|
||||
mMounts.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
try {
|
||||
File voldFile = new File("/system/etc/vold.fstab");
|
||||
if (voldFile.exists()){
|
||||
Scanner scanner = new Scanner(voldFile);
|
||||
while (scanner.hasNext()) {
|
||||
String line = scanner.nextLine();
|
||||
if (line.startsWith("dev_mount")) {
|
||||
String[] lineElements = line.split(" ");
|
||||
String element = lineElements[2];
|
||||
|
||||
if (element.contains(":"))
|
||||
element = element.substring(0, element.indexOf(":"));
|
||||
if (!element.equals("/mnt/sdcard"))
|
||||
mVold.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
for (int i = 0; i < mMounts.size(); i++) {
|
||||
String mount = mMounts.get(i);
|
||||
if (!mVold.contains(mount))
|
||||
mMounts.remove(i--);
|
||||
}
|
||||
mVold.clear();
|
||||
|
||||
List<String> mountHash = new ArrayList<>(10);
|
||||
|
||||
for (String mount : mMounts) {
|
||||
File root = new File(mount);
|
||||
if (root.exists() && root.isDirectory() && root.canRead()) {
|
||||
File[] list = root.listFiles();
|
||||
StringBuilder hash = new StringBuilder("[");
|
||||
if (list != null) {
|
||||
for (File f : list) {
|
||||
hash.append(f.getName().hashCode()).append(":").append(f.length()).append(", ");
|
||||
}
|
||||
}
|
||||
hash.append("]");
|
||||
if (!mountHash.contains(hash.toString())) {
|
||||
String key = SD_CARD + "_" + (map.size() / 2);
|
||||
if (map.size() == 0) {
|
||||
key = SD_CARD;
|
||||
} else if (map.size() == 2) {
|
||||
key = EXTERNAL_SD_CARD;
|
||||
}
|
||||
mountHash.add(hash.toString());
|
||||
map.add(key);
|
||||
map.add(root.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mMounts.clear();
|
||||
|
||||
if (Environment.getDataDirectory() != null
|
||||
&& !TextUtils.isEmpty(Environment.getDataDirectory().getAbsolutePath())) {
|
||||
File dataFilePath = new File(Environment.getDataDirectory().getAbsolutePath());
|
||||
if (dataFilePath.exists() && dataFilePath.isDirectory()) {
|
||||
map.add(DATA_DIRECTORY);
|
||||
map.add(Environment.getDataDirectory().getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
map.add(DATA_DIRECTORY_INT);
|
||||
map.add(ctx.getFilesDir().getPath());
|
||||
|
||||
if (ctx.getExternalFilesDir(null) != null) {
|
||||
map.add(DATA_DIRECTORY_EXT);
|
||||
map.add(ctx.getExternalFilesDir(null).getPath());
|
||||
}
|
||||
|
||||
// Now go through the external storage
|
||||
if (isAvailable()) { // we can read the External Storage...
|
||||
// Retrieve the primary External Storage:
|
||||
File primaryExternalStorage = Environment.getExternalStorageDirectory();
|
||||
|
||||
// Retrieve the External Storages root directory:
|
||||
String externalStorageRootDir;
|
||||
if ((externalStorageRootDir = primaryExternalStorage.getParent()) == null) { // no parent...
|
||||
String key = primaryExternalStorage.getAbsolutePath();
|
||||
if (!map.contains(key)) {
|
||||
map.add(key); // Make name as directory
|
||||
map.add(key);
|
||||
}
|
||||
} else {
|
||||
File externalStorageRoot = new File(externalStorageRootDir);
|
||||
File[] files = externalStorageRoot.listFiles();
|
||||
|
||||
if (files != null) {
|
||||
for (final File file : files) {
|
||||
// Check if it is a real directory (not a USB drive)...
|
||||
if (file.isDirectory()
|
||||
&& file.canRead()
|
||||
&& file.listFiles() != null
|
||||
&& (file.listFiles().length > 0)) {
|
||||
String key = file.getAbsolutePath();
|
||||
if (!map.contains(key)) {
|
||||
map.add(key); // Make name as directory
|
||||
map.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get candidates for removable external storage
|
||||
LinkedHashSet<File> candidateRemovableSdCardPaths = findSdCardPath();
|
||||
for (final File file : candidateRemovableSdCardPaths) {
|
||||
String key = file.getAbsolutePath();
|
||||
if (!map.contains(key)) {
|
||||
map.add(key); // Make name as directory
|
||||
map.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
346
backends/platform/android/org/scummvm/scummvm/INIParser.java
Normal file
346
backends/platform/android/org/scummvm/scummvm/INIParser.java
Normal file
@@ -0,0 +1,346 @@
|
||||
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.LinkedHashMap;
|
||||
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 LinkedHashMap<>();
|
||||
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 LinkedHashMap<>();
|
||||
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;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
168
backends/platform/android/org/scummvm/scummvm/LedView.java
Normal file
168
backends/platform/android/org/scummvm/scummvm/LedView.java
Normal file
@@ -0,0 +1,168 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class LedView extends View {
|
||||
public static final int DEFAULT_LED_COLOR = 0xffff0000;
|
||||
private static final int BLINK_TIME = 30; // ms
|
||||
|
||||
private boolean _state;
|
||||
private Runnable _blink;
|
||||
private Paint _painter;
|
||||
private int _radius;
|
||||
private int _centerX;
|
||||
private int _centerY;
|
||||
|
||||
public LedView(Context context) {
|
||||
this(context, true, DEFAULT_LED_COLOR);
|
||||
}
|
||||
|
||||
public LedView(Context context, boolean state) {
|
||||
this(context, state, DEFAULT_LED_COLOR);
|
||||
}
|
||||
|
||||
public LedView(Context context, boolean state, int color) {
|
||||
super(context);
|
||||
_state = state;
|
||||
init(color);
|
||||
}
|
||||
|
||||
public LedView(Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public LedView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP)
|
||||
public LedView(
|
||||
Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(
|
||||
attrs,
|
||||
R.styleable.LedView,
|
||||
defStyleAttr, defStyleRes);
|
||||
|
||||
try {
|
||||
_state = a.getBoolean(R.styleable.LedView_state, true);
|
||||
int color = a.getColor(R.styleable.LedView_color, DEFAULT_LED_COLOR);
|
||||
init(color);
|
||||
} finally {
|
||||
a.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private void init(int color) {
|
||||
_painter = new Paint();
|
||||
_painter.setStyle(Paint.Style.FILL);
|
||||
if (isInEditMode()) {
|
||||
_painter.setStrokeWidth(2);
|
||||
_painter.setStyle(_state ? Paint.Style.FILL : Paint.Style.STROKE);
|
||||
}
|
||||
_painter.setColor(color);
|
||||
_painter.setAntiAlias(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
|
||||
int w = resolveSizeAndState(minw, widthMeasureSpec, 0);
|
||||
|
||||
int minh = MeasureSpec.getSize(w) - getPaddingLeft() - getPaddingRight() +
|
||||
getPaddingBottom() + getPaddingTop();
|
||||
int h = resolveSizeAndState(minh, heightMeasureSpec, 0);
|
||||
|
||||
setMeasuredDimension(w, h);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
int xpad = (getPaddingLeft() + getPaddingRight());
|
||||
int ypad = (getPaddingTop() + getPaddingBottom());
|
||||
|
||||
int ww = w - xpad;
|
||||
int hh = h - ypad;
|
||||
|
||||
_radius = Math.min(ww, hh) / 2 - 2;
|
||||
_centerX = w / 2;
|
||||
_centerY = h / 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(@NonNull Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (!_state && !isInEditMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.drawCircle(_centerX, _centerY, _radius, _painter);
|
||||
}
|
||||
|
||||
public void on() {
|
||||
setState(true);
|
||||
}
|
||||
|
||||
public void off() {
|
||||
setState(false);
|
||||
}
|
||||
|
||||
public void setState(boolean state) {
|
||||
if (_blink != null) {
|
||||
removeCallbacks(_blink);
|
||||
_blink = null;
|
||||
}
|
||||
|
||||
if (_state == state) {
|
||||
return;
|
||||
}
|
||||
_state = state;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void blinkOnce() {
|
||||
if (_blink != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean oldState = _state;
|
||||
_state = !oldState;
|
||||
invalidate();
|
||||
|
||||
_blink = new Runnable() {
|
||||
private boolean _ran;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (_ran) {
|
||||
_blink = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_ran = true;
|
||||
_state = oldState;
|
||||
invalidate();
|
||||
|
||||
postDelayed(this, BLINK_TIME);
|
||||
}
|
||||
};
|
||||
postDelayed(_blink, BLINK_TIME);
|
||||
}
|
||||
}
|
||||
250
backends/platform/android/org/scummvm/scummvm/MouseHelper.java
Normal file
250
backends/platform/android/org/scummvm/scummvm/MouseHelper.java
Normal file
@@ -0,0 +1,250 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Contains helper methods for mouse/hover events that were introduced in Android 4.0.
|
||||
*/
|
||||
public class MouseHelper implements View.OnHoverListener {
|
||||
//private final View.OnHoverListener _listener;
|
||||
private final ScummVM _scummvm;
|
||||
private boolean _rmbPressed;
|
||||
private boolean _lmbPressed;
|
||||
private boolean _mmbPressed;
|
||||
private boolean _bmbPressed;
|
||||
private boolean _fmbPressed;
|
||||
private boolean _srmbPressed;
|
||||
private boolean _smmbPressed;
|
||||
|
||||
//
|
||||
// Class initialization fails when this throws an exception.
|
||||
// Checking hover availability is done on static class initialization for Android 1.6 compatibility.
|
||||
//
|
||||
static {
|
||||
try {
|
||||
Class.forName("android.view.View$OnHoverListener");
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling this forces class initialization
|
||||
*/
|
||||
public static void checkHoverAvailable() {}
|
||||
|
||||
public MouseHelper(ScummVM scummvm) {
|
||||
_scummvm = scummvm;
|
||||
//_listener = createListener();
|
||||
}
|
||||
|
||||
// private View.OnHoverListener createListener() {
|
||||
// return new View.OnHoverListener() {
|
||||
// @Override
|
||||
// public boolean onHover(View view, MotionEvent e) {
|
||||
// Log.d(ScummVM.LOG_TAG, "onHover mouseEvent");
|
||||
// return onMouseEvent(e, true);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
@Override
|
||||
public boolean onHover(View view, MotionEvent motionEvent) {
|
||||
//Log.d(ScummVM.LOG_TAG, "onHover mouseEvent");
|
||||
return onMouseEvent(motionEvent, true);
|
||||
// return false;
|
||||
}
|
||||
|
||||
// public void attach(SurfaceView main_surface) {
|
||||
// main_surface.setOnHoverListener(_listener);
|
||||
// }
|
||||
|
||||
// isTrackball is a subcase of isMouse (meaning isMouse will also return true)
|
||||
public static boolean isTrackball(KeyEvent e) {
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int source = e.getSource();
|
||||
return ((source & InputDevice.SOURCE_TRACKBALL) == InputDevice.SOURCE_TRACKBALL) ||
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && ((source & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE));
|
||||
|
||||
|
||||
}
|
||||
|
||||
// isTrackball is a subcase of isMouse (meaning isMouse will also return true)
|
||||
public static boolean isTrackball(MotionEvent e) {
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
//int source = e.getSource();
|
||||
|
||||
InputDevice device = e.getDevice();
|
||||
|
||||
if (device == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int sources = device.getSources();
|
||||
|
||||
return ((sources & InputDevice.SOURCE_TRACKBALL) == InputDevice.SOURCE_TRACKBALL) ||
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && ((sources & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE));
|
||||
|
||||
}
|
||||
|
||||
// "Checking against SOURCE_STYLUS only indicates "an input device is capable of obtaining input
|
||||
// from a stylus. To determine whether a given touch event was produced by a stylus, examine
|
||||
// the tool type returned by MotionEvent#getToolType(int) for each individual pointer."
|
||||
// https://developer.android.com/reference/android/view/InputDevice#SOURCE_STYLUS
|
||||
public static boolean isStylus(MotionEvent e){
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(int idx = 0; idx < e.getPointerCount(); idx++) {
|
||||
if (e.getToolType(idx) == MotionEvent.TOOL_TYPE_STYLUS)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isMouse(KeyEvent e) {
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int source = e.getSource();
|
||||
|
||||
//Log.d(ScummVM.LOG_TAG, "isMouse keyEvent source: " + source);
|
||||
|
||||
// SOURCE_MOUSE_RELATIVE is sent when mouse is detected as trackball
|
||||
// TODO: why does this happen? Do we need to also check for SOURCE_TRACKBALL here?
|
||||
return ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE)
|
||||
|| ((source & InputDevice.SOURCE_STYLUS) == InputDevice.SOURCE_STYLUS)
|
||||
|| ((source & InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD)
|
||||
|| isTrackball(e);
|
||||
}
|
||||
|
||||
public static boolean isMouse(MotionEvent e) {
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
InputDevice device = e.getDevice();
|
||||
|
||||
if (device == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int sources = device.getSources();
|
||||
|
||||
// SOURCE_MOUSE_RELATIVE is sent when mouse is detected as trackball
|
||||
// TODO: why does this happen? Do we need to also check for SOURCE_TRACKBALL here?
|
||||
// TODO: should these all be checks against TOOL_TYPEs instead of SOURCEs?
|
||||
return ((sources & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE)
|
||||
|| ((sources & InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD)
|
||||
|| isStylus(e)
|
||||
|| isTrackball(e);
|
||||
}
|
||||
|
||||
private boolean handleButton(MotionEvent e, boolean mbPressed, int mask, int downEvent, int upEvent) {
|
||||
boolean mbDown = (e.getButtonState() & mask) == mask;
|
||||
if ((e.getSource() & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE
|
||||
&& (e.getButtonState() & MotionEvent.BUTTON_BACK) == MotionEvent.BUTTON_BACK) {
|
||||
mbDown = (mask == MotionEvent.BUTTON_SECONDARY);
|
||||
}
|
||||
|
||||
if (mbDown) {
|
||||
if (!mbPressed) {
|
||||
// mouse button was pressed just now
|
||||
//Log.d(ScummVM.LOG_TAG, "handleButton mbDown, not mbPressed, mask = " + mask);
|
||||
_scummvm.pushEvent(downEvent, (int)e.getX(), (int)e.getY(), e.getButtonState(), 0, 0, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if (mbPressed) {
|
||||
//Log.d(ScummVM.LOG_TAG, "handleButton not mbDown, mbPressed, mask = " + mask);
|
||||
// mouse button was released just now
|
||||
_scummvm.pushEvent(upEvent, (int)e.getX(), (int)e.getY(), e.getButtonState(), 0, 0, 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
public boolean onMouseEvent(MotionEvent e, boolean hover) {
|
||||
|
||||
_scummvm.pushEvent(ScummVMEvents.JE_MOUSE_MOVE,
|
||||
(int) e.getX(),
|
||||
(int) e.getY(),
|
||||
0,
|
||||
0, 0, 0);
|
||||
|
||||
if (e.getActionMasked() == MotionEvent.ACTION_SCROLL) {
|
||||
// The call is coming from ScummVMEvents, from a GenericMotionEvent (scroll wheel movement)
|
||||
// TODO Do we want the JE_MOUSE_MOVE event too in this case?
|
||||
int eventJEWheelUpDown = ScummVMEvents.JE_MOUSE_WHEEL_UP;
|
||||
if (e.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0.0f) {
|
||||
eventJEWheelUpDown = ScummVMEvents.JE_MOUSE_WHEEL_DOWN;
|
||||
}
|
||||
//Log.d(ScummVM.LOG_TAG, "onMouseEvent Wheel Up/Down = " + eventJEWheelUpDown);
|
||||
_scummvm.pushEvent(eventJEWheelUpDown,
|
||||
(int) e.getX(),
|
||||
(int) e.getY(),
|
||||
0,
|
||||
0, 0, 0);
|
||||
} else {
|
||||
|
||||
int buttonState = e.getButtonState();
|
||||
|
||||
//Log.d(ScummVM.LOG_TAG, "onMouseEvent buttonState = " + buttonState);
|
||||
|
||||
boolean lmbDown = (buttonState & MotionEvent.BUTTON_PRIMARY) == MotionEvent.BUTTON_PRIMARY;
|
||||
|
||||
if (!hover && e.getActionMasked() != MotionEvent.ACTION_UP && buttonState == 0) {
|
||||
// On some device types, ButtonState is 0 even when tapping on the touch-pad or using the stylus on the screen etc.
|
||||
lmbDown = true;
|
||||
}
|
||||
|
||||
if (lmbDown) {
|
||||
if (!_lmbPressed) {
|
||||
// left mouse button was pressed just now
|
||||
_scummvm.pushEvent(ScummVMEvents.JE_LMB_DOWN, (int)e.getX(), (int)e.getY(), e.getButtonState(), 0, 0, 0);
|
||||
}
|
||||
|
||||
_lmbPressed = true;
|
||||
} else {
|
||||
if (_lmbPressed) {
|
||||
// left mouse button was released just now
|
||||
_scummvm.pushEvent(ScummVMEvents.JE_LMB_UP, (int)e.getX(), (int)e.getY(), e.getButtonState(), 0, 0, 0);
|
||||
}
|
||||
|
||||
_lmbPressed = false;
|
||||
}
|
||||
|
||||
_rmbPressed = handleButton(e, _rmbPressed, MotionEvent.BUTTON_SECONDARY, ScummVMEvents.JE_RMB_DOWN, ScummVMEvents.JE_RMB_UP);
|
||||
_mmbPressed = handleButton(e, _mmbPressed, MotionEvent.BUTTON_TERTIARY, ScummVMEvents.JE_MMB_DOWN, ScummVMEvents.JE_MMB_UP);
|
||||
_bmbPressed = handleButton(e, _bmbPressed, MotionEvent.BUTTON_BACK, ScummVMEvents.JE_BMB_DOWN, ScummVMEvents.JE_BMB_UP);
|
||||
_fmbPressed = handleButton(e, _fmbPressed, MotionEvent.BUTTON_FORWARD, ScummVMEvents.JE_FMB_DOWN, ScummVMEvents.JE_FMB_UP);
|
||||
// Lint warning for BUTTON_STYLUS... "
|
||||
// Field requires API level 23 (current min is 16): android.view.MotionEvent#BUTTON_STYLUS_PRIMARY"
|
||||
// Field requires API level 23 (current min is 16): android.view.MotionEvent#BUTTON_STYLUS_SECONDARY"
|
||||
// We suppress it:
|
||||
//
|
||||
// https://stackoverflow.com/a/48588149
|
||||
_srmbPressed = handleButton(e, _srmbPressed, MotionEvent.BUTTON_STYLUS_PRIMARY, ScummVMEvents.JE_RMB_DOWN, ScummVMEvents.JE_RMB_UP);
|
||||
_smmbPressed = handleButton(e, _smmbPressed, MotionEvent.BUTTON_STYLUS_SECONDARY, ScummVMEvents.JE_MMB_DOWN, ScummVMEvents.JE_MMB_UP);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import static org.scummvm.scummvm.ScummVMEvents.JE_MOUSE_WHEEL_DOWN;
|
||||
import static org.scummvm.scummvm.ScummVMEvents.JE_MOUSE_WHEEL_UP;
|
||||
import static org.scummvm.scummvm.ScummVMEvents.JE_MULTI;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class MultitouchHelper {
|
||||
private final ScummVM _scummvm;
|
||||
|
||||
private boolean _candidateStartOfMultitouchSession;
|
||||
// a flag indicating whether we are in multitouch mode (more than one fingers down)
|
||||
private boolean _multitouchMode;
|
||||
|
||||
// within a multitouch session (until a cancel event or no more multiple fingers down) the IDs for each finger-pointer is persisted and is consistent across events
|
||||
// we can use the ids to track and utilize the movement of a specific finger (while ignoring the rest)
|
||||
// Currently, we are using the last finger down, as the finger that moves the cursor
|
||||
private int _firstPointerId;
|
||||
private int _secondPointerId;
|
||||
private int _thirdPointerId;
|
||||
|
||||
private int _cachedActionEventOnPointer2DownX;
|
||||
private int _cachedActionEventOnPointer2DownY;
|
||||
|
||||
//
|
||||
private int _cachedActionEventOnPointer1DownX;
|
||||
private int _cachedActionEventOnPointer1DownY;
|
||||
|
||||
// The "level" of multitouch that is detected.
|
||||
// We do not support downgrading a level, ie. if a three finger multitouch is detected,
|
||||
// then raising one finger will void the multitouch session,
|
||||
// rather than revert to two fingers multitouch.
|
||||
// Similarly we do not support upgrading a level, ie. if we are already handling a two finger multitouch,
|
||||
// then putting down another finger will void the session,
|
||||
// rather than upgrade it to three fingers multitouch.
|
||||
// INFO for this purpose we need to allow some limited time limit (delay _kLevelDecisionDelayMs) before deciding
|
||||
// if the user did a two finger multitouch or intents to do a three finger multitouch
|
||||
// Valid values for _multitouchLevel: MULTITOUCH_UNDECIDED, MULTITOUCH_TWO_FINGERS, MULTITOUCH_THREE_FINGERS
|
||||
private final int MULTITOUCH_UNDECIDED = 0;
|
||||
private final int MULTITOUCH_TWO_FINGERS = 2;
|
||||
private final int MULTITOUCH_THREE_FINGERS = 3;
|
||||
private int _multitouchLevel;
|
||||
|
||||
private final int _kLevelDecisionDelayMs = 400; // in milliseconds
|
||||
private final int _kTouchMouseWheelDecisionDelayMs = 260; // in milliseconds - NOTE: Keep it significantly lower than _kLevelDecisionDelayMs
|
||||
|
||||
// messages for MultitouchHelperHandler
|
||||
final static int MSG_MT_DECIDE_MULTITOUCH_SESSION_TIMEDOUT = 1;
|
||||
final static int MSG_MT_UPGRADE_TO_LEVEL_3_TIMEDOUT = 2;
|
||||
final static int MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT = 3;
|
||||
|
||||
final private MultitouchHelper.MultitouchHelperHandler _multiTouchLevelUpgradeHandler = new MultitouchHelper.MultitouchHelperHandler(this);
|
||||
|
||||
// Scroll handling variables (calling it "Mouse Wheel" as SCROLL event on Android touch interface refers to moving around a finger on the touch surfaces)
|
||||
private final int TOUCH_MOUSE_WHEEL_UNDECIDED = 0;
|
||||
private final int TOUCH_MOUSE_WHEEL_ACTIVE = 1;
|
||||
private final int TOUCH_MOUSE_WHEEL_NOT_HAPPENING = 3;
|
||||
|
||||
// Both fingers need to have moved up or down to enter the scroll (mouse wheel) mode
|
||||
// The difference from the original positions (for both fingers respectively) is averaged and compared to the threshold below
|
||||
// The decision to enter scroll mode happens as long as there are two fingers down but we're still undecided,
|
||||
// (ie. undecided about whether this is a two finger event OR a third finger will follow OR it is a scroll event)
|
||||
private final int MOVE_THRESHOLD_FOR_TOUCH_MOUSE_WHEEL_DECISION = 20;
|
||||
|
||||
private final int MOVE_THRESHOLD_FOR_SEND_TOUCH_MOUSE_WHEEL_EVENT = 30;
|
||||
|
||||
// 0: Undecided for scrolling (mouse wheel)
|
||||
// 1: "scrolling" mode active
|
||||
// 2: no "scrolling" (mouse wheel) (decided)
|
||||
// Scrolling (mouse wheel) mode is mutually exclusive with the rest of the multi-touch modes,
|
||||
// we can either send mouse wheel events or mouse click events in a multitouch session.
|
||||
private int _touchMouseWheelDecisionLevel = 0;
|
||||
|
||||
// constructor
|
||||
public MultitouchHelper(ScummVM scummvm) {
|
||||
_scummvm = scummvm;
|
||||
|
||||
_multitouchMode = false;
|
||||
_multitouchLevel = MULTITOUCH_UNDECIDED;
|
||||
_candidateStartOfMultitouchSession = false;
|
||||
|
||||
_touchMouseWheelDecisionLevel = TOUCH_MOUSE_WHEEL_UNDECIDED;
|
||||
resetPointers();
|
||||
}
|
||||
|
||||
public void resetPointers() {
|
||||
_firstPointerId = -1;
|
||||
_secondPointerId = -1;
|
||||
_thirdPointerId = -1;
|
||||
_cachedActionEventOnPointer1DownX = -1;
|
||||
_cachedActionEventOnPointer1DownY = -1;
|
||||
_cachedActionEventOnPointer2DownX = -1;
|
||||
_cachedActionEventOnPointer2DownY = -1;
|
||||
}
|
||||
|
||||
public boolean isMultitouchMode() {
|
||||
return _multitouchMode;
|
||||
}
|
||||
public int getMultitouchLevel() {
|
||||
return _multitouchLevel;
|
||||
}
|
||||
|
||||
public int getTouchMouseWheelDecisionLevel() {
|
||||
return _touchMouseWheelDecisionLevel;
|
||||
}
|
||||
public boolean isTouchMouseWheel() { return getTouchMouseWheelDecisionLevel() == TOUCH_MOUSE_WHEEL_ACTIVE; }
|
||||
|
||||
public void setMultitouchMode(boolean enabledFlg) {
|
||||
_multitouchMode = enabledFlg;
|
||||
}
|
||||
public void setMultitouchLevel(int mtlevel) {
|
||||
_multitouchLevel = mtlevel;
|
||||
}
|
||||
|
||||
public void setTouchMouseWheelDecisionLevel(int scrlevel) {
|
||||
_touchMouseWheelDecisionLevel = scrlevel;
|
||||
}
|
||||
|
||||
// TODO Maybe for consistency purposes, maybe sent all (important) UP events that were not sent, when ending a multitouch session?
|
||||
public boolean handleMotionEvent(final MotionEvent event) {
|
||||
|
||||
// constants from APIv5:
|
||||
// (action & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT
|
||||
//final int pointer = (action & 0xff00) >> 8;
|
||||
|
||||
final int maskedAction = event.getActionMasked();
|
||||
|
||||
int pointerIndex = -1;
|
||||
int actionEventX;
|
||||
int actionEventY;
|
||||
|
||||
if (maskedAction == MotionEvent.ACTION_DOWN) {
|
||||
// start of a multitouch session! one finger down -- this is sent for the first pointer who touches the screen
|
||||
resetPointers();
|
||||
setMultitouchLevel(MULTITOUCH_UNDECIDED);
|
||||
setTouchMouseWheelDecisionLevel(TOUCH_MOUSE_WHEEL_UNDECIDED);
|
||||
setMultitouchMode(false);
|
||||
_candidateStartOfMultitouchSession = true;
|
||||
_multiTouchLevelUpgradeHandler.clear();
|
||||
|
||||
pointerIndex = 0;
|
||||
_firstPointerId = event.getPointerId(pointerIndex);
|
||||
_cachedActionEventOnPointer1DownX = (int) event.getX(pointerIndex);;
|
||||
_cachedActionEventOnPointer1DownY = (int) event.getY(pointerIndex);;
|
||||
return false;
|
||||
|
||||
} else if (maskedAction == MotionEvent.ACTION_CANCEL) {
|
||||
resetPointers();
|
||||
setMultitouchLevel(MULTITOUCH_UNDECIDED);
|
||||
setTouchMouseWheelDecisionLevel(TOUCH_MOUSE_WHEEL_UNDECIDED);
|
||||
setMultitouchMode(false);
|
||||
_multiTouchLevelUpgradeHandler.clear();
|
||||
return true;
|
||||
|
||||
} else if (maskedAction == MotionEvent.ACTION_OUTSIDE) {
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
if (event.getPointerCount() > 1 && event.getPointerCount() < 4) {
|
||||
// a multi-touch event
|
||||
if (_candidateStartOfMultitouchSession && event.getPointerCount() > 1) {
|
||||
_candidateStartOfMultitouchSession = false; // reset this flag
|
||||
setMultitouchMode(true);
|
||||
}
|
||||
|
||||
if (isMultitouchMode()) {
|
||||
if (maskedAction == MotionEvent.ACTION_POINTER_DOWN) {
|
||||
pointerIndex = event.getActionIndex();
|
||||
if (event.getPointerCount() == 2) {
|
||||
_secondPointerId = event.getPointerId(pointerIndex);
|
||||
|
||||
if (getMultitouchLevel() == MULTITOUCH_UNDECIDED) {
|
||||
_multiTouchLevelUpgradeHandler.removeMessages(MSG_MT_UPGRADE_TO_LEVEL_3_TIMEDOUT);
|
||||
_multiTouchLevelUpgradeHandler.removeMessages(MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT);
|
||||
if (pointerIndex != -1) {
|
||||
_cachedActionEventOnPointer2DownX = (int) event.getX(pointerIndex);
|
||||
_cachedActionEventOnPointer2DownY = (int) event.getY(pointerIndex);
|
||||
} else {
|
||||
_cachedActionEventOnPointer2DownX = -1;
|
||||
_cachedActionEventOnPointer2DownY = -1;
|
||||
}
|
||||
// Allow for some time before deciding a two finger touch event, since the user might be going for a three finger touch event
|
||||
_multiTouchLevelUpgradeHandler.sendMessageDelayed(_multiTouchLevelUpgradeHandler.obtainMessage(MSG_MT_UPGRADE_TO_LEVEL_3_TIMEDOUT), _kLevelDecisionDelayMs);
|
||||
// Also allow for (less) time to check if this is a two-finger "mouse-wheel" event
|
||||
_multiTouchLevelUpgradeHandler.sendMessageDelayed(_multiTouchLevelUpgradeHandler.obtainMessage(MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT), _kTouchMouseWheelDecisionDelayMs);
|
||||
// Return as event "handled" here
|
||||
// while we wait for the decision to be made for the level of multitouch (two or three)
|
||||
//
|
||||
return true;
|
||||
}
|
||||
// Don't return here
|
||||
// We want to handle the case whereby we were in multitouch level 2, and we got a new pointer down event with 2 pointers count
|
||||
// This is the case where the user keeps one finger down and taps the second finger
|
||||
// This behavior should count as multiple right clicks (one for each new "tap" (ACTION_POINTER_DOWN event))
|
||||
// for user friendliness / control intuitiveness
|
||||
|
||||
} else if (event.getPointerCount() == 3) {
|
||||
_thirdPointerId = event.getPointerId(pointerIndex);
|
||||
if (getMultitouchLevel() == MULTITOUCH_UNDECIDED) {
|
||||
setMultitouchLevel(MULTITOUCH_THREE_FINGERS);
|
||||
_multiTouchLevelUpgradeHandler.removeMessages(MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT);
|
||||
setTouchMouseWheelDecisionLevel(TOUCH_MOUSE_WHEEL_NOT_HAPPENING);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (event.getPointerCount() == 2) {
|
||||
// we prioritize the second pointer/ finger
|
||||
pointerIndex = event.findPointerIndex(_secondPointerId);
|
||||
if (pointerIndex != -1) {
|
||||
actionEventX = (int)event.getX(pointerIndex);
|
||||
actionEventY = (int)event.getY(pointerIndex);
|
||||
} else {
|
||||
actionEventX = -1;
|
||||
actionEventY = -1;
|
||||
}
|
||||
|
||||
if (getMultitouchLevel() == MULTITOUCH_UNDECIDED) {
|
||||
// Fast trigger an ACTION_POINTER_DOWN if:
|
||||
// - we were not yet decided on which level to use
|
||||
// AND either:
|
||||
// - a finger got up (from 3 to 2, shouldn't happen) or
|
||||
// - our main finger (second finger down) moved from cached position
|
||||
if (maskedAction == MotionEvent.ACTION_POINTER_UP
|
||||
|| (maskedAction == MotionEvent.ACTION_MOVE
|
||||
&& (actionEventX != _cachedActionEventOnPointer2DownX
|
||||
|| actionEventY != _cachedActionEventOnPointer2DownY))) {
|
||||
|
||||
setMultitouchLevel(MULTITOUCH_TWO_FINGERS);
|
||||
_multiTouchLevelUpgradeHandler.removeMessages(MSG_MT_UPGRADE_TO_LEVEL_3_TIMEDOUT);
|
||||
|
||||
// Checking if we can decide on a touch mouse wheel mode session
|
||||
int firstPointerIndex = event.findPointerIndex(_firstPointerId);
|
||||
int actionEventFirstPointerCoordY = -1;
|
||||
if (firstPointerIndex != -1) {
|
||||
actionEventFirstPointerCoordY = (int) event.getY(firstPointerIndex);
|
||||
}
|
||||
|
||||
if (maskedAction == MotionEvent.ACTION_MOVE) {
|
||||
// Decide Scroll (touch mouse wheel) if:
|
||||
// - we were not yet decided on which level to use
|
||||
// - and two fingers are down (but not because we went from 3 to 2)
|
||||
// - and it's a move event
|
||||
// - 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),
|
||||
// since it's in reference to the starting points for the fingers
|
||||
int movementOfFinger2onY = actionEventY - _cachedActionEventOnPointer2DownY;
|
||||
int movementOfFinger1onY = actionEventFirstPointerCoordY - _cachedActionEventOnPointer1DownY;
|
||||
int absMovementOfFinger2onY = Math.abs(movementOfFinger2onY);
|
||||
int absMovementOfFinger1onY = Math.abs(movementOfFinger1onY);
|
||||
int absDiffOfMovementOfFingersOnY = Math.abs(movementOfFinger2onY - movementOfFinger1onY);
|
||||
|
||||
if (getTouchMouseWheelDecisionLevel() == TOUCH_MOUSE_WHEEL_UNDECIDED
|
||||
&& (movementOfFinger2onY > 0 && movementOfFinger1onY > 0) || (movementOfFinger2onY < 0 && movementOfFinger1onY < 0)
|
||||
&& absDiffOfMovementOfFingersOnY < MOVE_THRESHOLD_FOR_TOUCH_MOUSE_WHEEL_DECISION ) {
|
||||
|
||||
if ((absMovementOfFinger2onY + absMovementOfFinger1onY) / 2 >= MOVE_THRESHOLD_FOR_TOUCH_MOUSE_WHEEL_DECISION) {
|
||||
setTouchMouseWheelDecisionLevel(TOUCH_MOUSE_WHEEL_ACTIVE);
|
||||
_multiTouchLevelUpgradeHandler.removeMessages(MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT);
|
||||
} else {
|
||||
// ignore this move event but don't forward it (return true as "event handled")
|
||||
// there's still potential to be a scroll (touch mouse wheel) event, with accumulated movement
|
||||
//
|
||||
// Also downgrade the multitouch level to undecided to re-enter this code segment next time
|
||||
// (the "countdown" for three-finger touch decision is not resumed)
|
||||
setMultitouchLevel(MULTITOUCH_UNDECIDED);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
setMultitouchLevel(MULTITOUCH_UNDECIDED);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
setTouchMouseWheelDecisionLevel(TOUCH_MOUSE_WHEEL_NOT_HAPPENING);
|
||||
_multiTouchLevelUpgradeHandler.removeMessages(MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT);
|
||||
}
|
||||
// End of: Checking if we can decide on a touch mouse wheel mode session
|
||||
|
||||
if (getTouchMouseWheelDecisionLevel() != TOUCH_MOUSE_WHEEL_ACTIVE) {
|
||||
// send the missing pointer down event first, before sending the actual current move event below
|
||||
_scummvm.pushEvent(JE_MULTI,
|
||||
event.getPointerCount(),
|
||||
MotionEvent.ACTION_POINTER_DOWN,
|
||||
actionEventX,
|
||||
actionEventY,
|
||||
0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (event.getPointerCount() == 3) {
|
||||
// we prioritize the third pointer/ finger
|
||||
pointerIndex = event.findPointerIndex(_thirdPointerId);
|
||||
}
|
||||
}
|
||||
|
||||
// if (pointerIndex == -1) {
|
||||
// Log.d(ScummVM.LOG_TAG,"Warning: pointerIndex == -1 and getPointerCount = " + event.getPointerCount());
|
||||
// }
|
||||
|
||||
if (pointerIndex != -1) {
|
||||
actionEventX = (int)event.getX(pointerIndex);
|
||||
actionEventY = (int)event.getY(pointerIndex);
|
||||
} else {
|
||||
actionEventX = -1;
|
||||
actionEventY = -1;
|
||||
}
|
||||
|
||||
// we are only concerned for events with fingers down equal to the decided level of multitouch session
|
||||
if (getMultitouchLevel() == event.getPointerCount()) {
|
||||
if ((isTouchMouseWheel()) ) {
|
||||
if (maskedAction == MotionEvent.ACTION_MOVE && pointerIndex == event.findPointerIndex(_secondPointerId)) {
|
||||
// The co-ordinates sent with the event are the original touch co-ordinates of the first finger
|
||||
// The mouse cursor should not move around while touch-mouse-wheel scrolling
|
||||
// Also for simplification we only use the move events for the second finger
|
||||
// and skip non-significant movements.
|
||||
int movementOfFinger2onY = actionEventY - _cachedActionEventOnPointer2DownY;
|
||||
if (Math.abs(movementOfFinger2onY) > MOVE_THRESHOLD_FOR_SEND_TOUCH_MOUSE_WHEEL_EVENT) {
|
||||
_scummvm.pushEvent((movementOfFinger2onY > 0 ) ? JE_MOUSE_WHEEL_UP : JE_MOUSE_WHEEL_DOWN,
|
||||
_cachedActionEventOnPointer1DownX,
|
||||
_cachedActionEventOnPointer1DownY,
|
||||
1, // This will indicate to the event handling code in native that it comes from touch interface
|
||||
0,
|
||||
0, 0);
|
||||
_cachedActionEventOnPointer2DownY = actionEventY;
|
||||
}
|
||||
} // otherwise don't push an event in this case and return true
|
||||
} else {
|
||||
// arg1 will be the number of fingers down in the MULTI event we send to events.cpp
|
||||
// arg2 is the event action
|
||||
// arg3 and arg4 are the X,Y coordinates for the "active pointer" index, which is the last finger down
|
||||
// (the second in two-fingers touch, or the third in a three-fingers touch mode)
|
||||
_scummvm.pushEvent(JE_MULTI,
|
||||
event.getPointerCount(),
|
||||
event.getAction(),
|
||||
actionEventX,
|
||||
actionEventY,
|
||||
0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if (event.getPointerCount() >= 4) {
|
||||
// ignore if more than 3 fingers down. Mark as multitouch "handled" (return true)
|
||||
return true;
|
||||
|
||||
} else if (event.getPointerCount() == 1 && isMultitouchMode() ) {
|
||||
// We were already in a Multitouch session, but we're left with one finger down now
|
||||
// Keep ignoring events for single pointer until we exit multitouch mode "session"
|
||||
// this is to catch the case of being left with only one finger still touching the surface
|
||||
// after lifting the rest of the fingers that were touching the surface
|
||||
return true;
|
||||
} else {
|
||||
// one finger, no active multitouch mode "session". Mark as unhandled.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom handler code (to avoid mem leaks, see warning "This Handler Class Should Be Static Or Leaks Might Occur”) based on:
|
||||
// https://stackoverflow.com/a/27826094
|
||||
public static class MultitouchHelperHandler extends Handler {
|
||||
|
||||
private final WeakReference<MultitouchHelper> mListenerReference;
|
||||
|
||||
public MultitouchHelperHandler(MultitouchHelper listener) {
|
||||
super(Looper.getMainLooper());
|
||||
mListenerReference = new WeakReference<>(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void handleMessage(@NonNull Message msg) {
|
||||
MultitouchHelper listener = mListenerReference.get();
|
||||
if(listener != null) {
|
||||
listener.handle_MTHH_Message(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.removeCallbacksAndMessages(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void handle_MTHH_Message(final Message msg) {
|
||||
if ((msg.what == MSG_MT_UPGRADE_TO_LEVEL_3_TIMEDOUT && getMultitouchLevel() == MULTITOUCH_UNDECIDED)
|
||||
|| (msg.what == MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT
|
||||
&& (getMultitouchLevel() == MULTITOUCH_UNDECIDED
|
||||
|| (getMultitouchLevel() == 2 && getTouchMouseWheelDecisionLevel() == TOUCH_MOUSE_WHEEL_UNDECIDED)))) {
|
||||
// Either:
|
||||
// - window of allowing upgrade to level 3 (three fingers) timed out
|
||||
// - or window of allowing time for checking for scroll (touch mouse wheel) timed out
|
||||
// decide level 2 (two fingers).
|
||||
setMultitouchLevel(MULTITOUCH_TWO_FINGERS);
|
||||
setTouchMouseWheelDecisionLevel(TOUCH_MOUSE_WHEEL_NOT_HAPPENING);
|
||||
|
||||
// send the delayed pointer down event
|
||||
_scummvm.pushEvent(JE_MULTI,
|
||||
2,
|
||||
MotionEvent.ACTION_POINTER_DOWN,
|
||||
_cachedActionEventOnPointer2DownX,
|
||||
_cachedActionEventOnPointer2DownY,
|
||||
0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearEventHandler() {
|
||||
_multiTouchLevelUpgradeHandler.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
public interface OnKeyboardVisibilityListener {
|
||||
void onVisibilityChanged(boolean visible);
|
||||
}
|
||||
797
backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
Normal file
797
backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
Normal file
@@ -0,0 +1,797 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.UriPermission;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.system.OsConstants;
|
||||
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;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* SAF primitives for C++ FSNode
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public class SAFFSTree {
|
||||
@RequiresApi(api = Build.VERSION_CODES.BASE)
|
||||
public interface IOBusyListener {
|
||||
void onIOBusy(float ratio);
|
||||
}
|
||||
|
||||
private static class IOTime {
|
||||
long start;
|
||||
long end;
|
||||
long duration;
|
||||
}
|
||||
// Declare us as busy when I/O waits took more than 90% in 2 secs
|
||||
private static final long IO_BUSINESS_TIMESPAN = 2000;
|
||||
private static final long IO_BUSINESS_THRESHOLD = 1800;
|
||||
|
||||
private static ConcurrentLinkedQueue<IOTime> _lastIOs;
|
||||
private static IOBusyListener _listener;
|
||||
|
||||
private static HashMap<String, SAFFSTree> _trees;
|
||||
|
||||
// This map will store the references of all our objects used
|
||||
// by the native side.
|
||||
// This avoids overflowing JNI will a pile of global references
|
||||
private static ConcurrentHashMap<Long, SAFFSNode> _nodes;
|
||||
// This atomic variable will generate unique identifiers for our objects
|
||||
private static AtomicLong _idCounter;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.BASE)
|
||||
public static void setIOBusyListener(IOBusyListener l) {
|
||||
if (_lastIOs == null) {
|
||||
_lastIOs = new ConcurrentLinkedQueue<>();
|
||||
}
|
||||
_listener = l;
|
||||
}
|
||||
|
||||
private static void reportIO(long start, long end) {
|
||||
if (_listener == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register this new query
|
||||
IOTime entry = new IOTime();
|
||||
entry.start = start;
|
||||
entry.end = end;
|
||||
entry.duration = end - start;
|
||||
_lastIOs.add(entry);
|
||||
|
||||
long deadline = end - IO_BUSINESS_TIMESPAN;
|
||||
long duration = 0;
|
||||
|
||||
// Remove outdated entries and compute the time spent in I/Os
|
||||
Iterator<IOTime> it = _lastIOs.iterator();
|
||||
while (it.hasNext()) {
|
||||
entry = it.next();
|
||||
//Log.d(ScummVM.LOG_TAG, "ENTRY <" + Long.toString(entry.start) + " " + Long.toString(entry.end) + " " + Long.toString(entry.duration) + ">");
|
||||
if (entry.end <= deadline) {
|
||||
// entry is too old
|
||||
it.remove();
|
||||
} else if (entry.start < deadline) {
|
||||
// This entry crossed the deadline
|
||||
duration += entry.end - deadline;
|
||||
} else {
|
||||
duration += entry.duration;
|
||||
}
|
||||
}
|
||||
//Log.d(ScummVM.LOG_TAG, "SUM: " + Long.toString(duration) + " DEADLINE WAS: " + Long.toString(deadline));
|
||||
|
||||
if (duration >= IO_BUSINESS_THRESHOLD && _listener != null) {
|
||||
_listener.onIOBusy((float)duration / IO_BUSINESS_TIMESPAN);
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadSAFTrees(Context context) {
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
|
||||
// As this function is called before starting to emit nodes,
|
||||
// we can take the opportunity to setup the reference related stuff here
|
||||
if (_nodes == null) {
|
||||
_nodes = new ConcurrentHashMap<>();
|
||||
_idCounter = new AtomicLong();
|
||||
}
|
||||
|
||||
_trees = new HashMap<>();
|
||||
for (UriPermission permission : resolver.getPersistedUriPermissions()) {
|
||||
final Uri uri = permission.getUri();
|
||||
if (!DocumentsContract.isTreeUri(uri)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SAFFSTree tree = new SAFFSTree(context, uri);
|
||||
_trees.put(tree.getTreeId(), tree);
|
||||
}
|
||||
}
|
||||
|
||||
public static SAFFSTree newTree(Context context, Uri uri) {
|
||||
if (_trees == null) {
|
||||
loadSAFTrees(context);
|
||||
}
|
||||
SAFFSTree tree = new SAFFSTree(context, uri);
|
||||
_trees.put(tree.getTreeId(), tree);
|
||||
return tree;
|
||||
}
|
||||
|
||||
public static SAFFSTree[] getTrees(Context context) {
|
||||
if (_trees == null) {
|
||||
loadSAFTrees(context);
|
||||
}
|
||||
return _trees.values().toArray(new SAFFSTree[0]);
|
||||
}
|
||||
|
||||
public static SAFFSTree findTree(Context context, String name) {
|
||||
if (_trees == null) {
|
||||
loadSAFTrees(context);
|
||||
}
|
||||
return _trees.get(name);
|
||||
}
|
||||
|
||||
public static class PathResult {
|
||||
public final SAFFSTree tree;
|
||||
public final SAFFSNode node;
|
||||
|
||||
PathResult(SAFFSTree tree, SAFFSNode node) {
|
||||
this.tree = tree;
|
||||
this.node = node;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a ScummVM virtual path to SAF objects if it's in the SAF domain.
|
||||
* Returns null otherwise and throws a FileNotFoundException if the SAF path doesn't exist.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.BASE)
|
||||
public static PathResult fullPathToNode(Context context, String path, boolean createDirIfNotExists) throws FileNotFoundException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
|
||||
!path.startsWith("/saf/")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// This is a SAF fake mount point
|
||||
int slash = path.indexOf('/', 5);
|
||||
if (slash == -1) {
|
||||
slash = path.length();
|
||||
}
|
||||
String treeName = path.substring(5, slash);
|
||||
String innerPath = path.substring(slash);
|
||||
|
||||
SAFFSTree tree = SAFFSTree.findTree(context, treeName);
|
||||
if (tree == null) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
SAFFSNode node = tree.pathToNode(innerPath, createDirIfNotExists);
|
||||
if (node == null) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
return new PathResult(tree, node);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.BASE)
|
||||
public static void clearCaches() {
|
||||
if (_trees == null) {
|
||||
return;
|
||||
}
|
||||
for (SAFFSTree tree : _trees.values()) {
|
||||
tree.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public static void addNodeRef(long nodeId) {
|
||||
assert(nodeId != 0);
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
long newId = node.addRef();
|
||||
assert(newId == nodeId);
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public static void decNodeRef(long nodeId) {
|
||||
assert(nodeId != 0);
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
node.decRef();
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public static SAFFSNode refToNode(long nodeId) {
|
||||
assert(nodeId != 0);
|
||||
return _nodes.get(nodeId);
|
||||
}
|
||||
|
||||
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;
|
||||
public static final int DELETABLE = 0x08;
|
||||
public static final int REMOVABLE = 0x10;
|
||||
|
||||
public SAFFSNode _parent;
|
||||
public String _path;
|
||||
public String _documentId;
|
||||
public int _flags;
|
||||
|
||||
private HashMap<String, SoftReference<SAFFSNode>> _children;
|
||||
private boolean _dirty;
|
||||
private int _refCnt; // Reference counter for the native side
|
||||
private long _id; // Identifier for the native side
|
||||
|
||||
private SAFFSNode reset(SAFFSNode parent, String path, String documentId, int flags) {
|
||||
_parent = parent;
|
||||
_path = path;
|
||||
_documentId = documentId;
|
||||
_flags = flags;
|
||||
|
||||
_children = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
private static int computeFlags(String mimeType, int flags) {
|
||||
int ourFlags = 0;
|
||||
if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||
ourFlags |= SAFFSNode.DIRECTORY;
|
||||
}
|
||||
if ((flags & (DocumentsContract.Document.FLAG_SUPPORTS_WRITE | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE)) != 0) {
|
||||
ourFlags |= SAFFSNode.WRITABLE;
|
||||
}
|
||||
if ((flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) == 0) {
|
||||
ourFlags |= SAFFSNode.READABLE;
|
||||
}
|
||||
if ((flags & DocumentsContract.Document.FLAG_SUPPORTS_DELETE) != 0) {
|
||||
ourFlags |= SAFFSNode.DELETABLE;
|
||||
}
|
||||
if ((flags & DocumentsContract.Document.FLAG_SUPPORTS_REMOVE) != 0) {
|
||||
ourFlags |= SAFFSNode.REMOVABLE;
|
||||
}
|
||||
return ourFlags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(SAFFSNode o) {
|
||||
if (o == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
return _path.compareTo(o._path);
|
||||
}
|
||||
|
||||
public synchronized long addRef() {
|
||||
_refCnt += 1;
|
||||
if (_refCnt > 1) {
|
||||
return _id;
|
||||
}
|
||||
assert(_refCnt == 1);
|
||||
|
||||
if (_id == 0) {
|
||||
_id = _idCounter.incrementAndGet();
|
||||
}
|
||||
_nodes.put(_id, this);
|
||||
|
||||
return _id;
|
||||
}
|
||||
|
||||
public synchronized void decRef() {
|
||||
if (_refCnt == 1) {
|
||||
SAFFSNode tmp = _nodes.remove(_id);
|
||||
assert(tmp == this);
|
||||
}
|
||||
_refCnt -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
private final Context _context;
|
||||
private final Uri _treeUri;
|
||||
|
||||
private final SAFFSNode _root;
|
||||
private final String _treeName;
|
||||
|
||||
public SAFFSTree(Context context, Uri treeUri) {
|
||||
_context = context;
|
||||
_treeUri = treeUri;
|
||||
|
||||
_root = new SAFFSNode().reset(null, "", DocumentsContract.getTreeDocumentId(treeUri), 0);
|
||||
// Update flags and get name
|
||||
String treeName = stat(_root);
|
||||
if (treeName == null) {
|
||||
// The tree likely got deleted
|
||||
// Use the document ID instead as this will let the user do some cleanup
|
||||
treeName = DocumentsContract.getTreeDocumentId(treeUri);
|
||||
}
|
||||
_treeName = treeName;
|
||||
}
|
||||
|
||||
public String getTreeId() {
|
||||
return Uri.encode(DocumentsContract.getTreeDocumentId(_treeUri));
|
||||
}
|
||||
public String getTreeName() {
|
||||
return _treeName;
|
||||
}
|
||||
public Uri getTreeDocumentUri() {
|
||||
return DocumentsContract.buildDocumentUriUsingTree(_treeUri, _root._documentId);
|
||||
}
|
||||
|
||||
private void clearCache() {
|
||||
ArrayDeque<SAFFSNode> stack = new ArrayDeque<>();
|
||||
stack.push(_root);
|
||||
while (!stack.isEmpty()) {
|
||||
SAFFSNode node = stack.pop();
|
||||
node._dirty = true;
|
||||
if (node._children == null) {
|
||||
continue;
|
||||
}
|
||||
for (SoftReference<SAFFSNode> ref : node._children.values()) {
|
||||
node = ref.get();
|
||||
if (node != null) {
|
||||
stack.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SAFFSNode pathToNode(String path, boolean createDirIfNotExists) {
|
||||
String[] components = path.split("/");
|
||||
|
||||
SAFFSNode node = _root;
|
||||
for (String component : components) {
|
||||
if (component.isEmpty() || ".".equals(component)) {
|
||||
continue;
|
||||
}
|
||||
if ("..".equals(component)) {
|
||||
if (node._parent != null) {
|
||||
node = node._parent;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
SAFFSNode newNode = getChild(node, component);
|
||||
if (newNode == null && createDirIfNotExists) {
|
||||
newNode = createDirectory(node, component);
|
||||
}
|
||||
if (newNode == null) {
|
||||
return null;
|
||||
}
|
||||
node = newNode;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
public SAFFSNode[] getChildren(SAFFSNode node) {
|
||||
Collection<SAFFSNode> results = null;
|
||||
if (node._children != null && !node._dirty) {
|
||||
results = new ArrayDeque<>();
|
||||
for (SoftReference<SAFFSNode> ref : node._children.values()) {
|
||||
if (ref == null) {
|
||||
continue;
|
||||
}
|
||||
SAFFSNode newnode = ref.get();
|
||||
if (newnode == null) {
|
||||
// Some reference went stale: refresh
|
||||
results = null;
|
||||
break;
|
||||
}
|
||||
results.add(newnode);
|
||||
}
|
||||
}
|
||||
|
||||
if (results == null) {
|
||||
try {
|
||||
results = fetchChildren(node);
|
||||
} catch (Exception e) {
|
||||
Log.w(ScummVM.LOG_TAG, "Failed to get children: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return results.toArray(new SAFFSNode[0]);
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public SAFFSNode[] getChildren(long nodeId) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return getChildren(node);
|
||||
}
|
||||
|
||||
public Collection<SAFFSNode> fetchChildren(SAFFSNode node) {
|
||||
final ContentResolver resolver = _context.getContentResolver();
|
||||
final Uri searchUri = DocumentsContract.buildChildDocumentsUriUsingTree(_treeUri, node._documentId);
|
||||
final ArrayDeque<SAFFSNode> results = new ArrayDeque<>();
|
||||
|
||||
// Keep the old children around to reuse them: this will help to keep one SAFFSNode instance for each node
|
||||
HashMap<String, SoftReference<SAFFSNode>> oldChildren = node._children;
|
||||
node._children = null;
|
||||
|
||||
// When _children will be set, it will be clean
|
||||
node._dirty = false;
|
||||
HashMap<String, SoftReference<SAFFSNode>> newChildren = new HashMap<>();
|
||||
|
||||
Cursor c = null;
|
||||
|
||||
long startIO = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
c = resolver.query(searchUri, new String[] { DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||
DocumentsContract.Document.COLUMN_FLAGS }, null, null, null);
|
||||
if (c == null) {
|
||||
return results;
|
||||
}
|
||||
while (c.moveToNext()) {
|
||||
final String displayName = c.getString(0);
|
||||
final String documentId = c.getString(1);
|
||||
final String mimeType = c.getString(2);
|
||||
final int flags = c.getInt(3);
|
||||
|
||||
final int ourFlags = SAFFSNode.computeFlags(mimeType, flags);
|
||||
|
||||
SAFFSNode newnode = null;
|
||||
SoftReference<SAFFSNode> oldnodeRef;
|
||||
if (oldChildren != null) {
|
||||
oldnodeRef = oldChildren.remove(displayName);
|
||||
if (oldnodeRef != null) {
|
||||
newnode = oldnodeRef.get();
|
||||
}
|
||||
}
|
||||
if (newnode == null) {
|
||||
newnode = new SAFFSNode();
|
||||
}
|
||||
|
||||
newnode.reset(node, node._path + "/" + displayName, documentId, ourFlags);
|
||||
newChildren.put(displayName, new SoftReference<>(newnode));
|
||||
|
||||
results.add(newnode);
|
||||
}
|
||||
|
||||
// Success: store the cache
|
||||
node._children = newChildren;
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
|
||||
long endIO = System.currentTimeMillis();
|
||||
reportIO(startIO, endIO);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public SAFFSNode getChild(SAFFSNode node, String name) {
|
||||
//This variable is used to hold a strong reference on every children nodes
|
||||
//noinspection unused
|
||||
Collection<SAFFSNode> children;
|
||||
|
||||
if (node._children == null || node._dirty) {
|
||||
try {
|
||||
//noinspection UnusedAssignment
|
||||
children = fetchChildren(node);
|
||||
} catch (Exception e) {
|
||||
Log.w(ScummVM.LOG_TAG, "Failed to get children: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
SoftReference<SAFFSNode> ref = node._children.get(name);
|
||||
if (ref == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SAFFSNode newnode = ref.get();
|
||||
if (newnode != null) {
|
||||
return newnode;
|
||||
}
|
||||
|
||||
// Node reference was stale, force a refresh
|
||||
try {
|
||||
//noinspection UnusedAssignment
|
||||
children = fetchChildren(node);
|
||||
} catch (Exception e) {
|
||||
Log.w(ScummVM.LOG_TAG, "Failed to get children: " + e);
|
||||
return null;
|
||||
}
|
||||
|
||||
ref = node._children.get(name);
|
||||
if (ref == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
newnode = ref.get();
|
||||
if (newnode == null) {
|
||||
Log.e(ScummVM.LOG_TAG, "Failed to keep a reference on object");
|
||||
}
|
||||
|
||||
return newnode;
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public SAFFSNode getChild(long nodeId, String name) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return getChild(node, name);
|
||||
}
|
||||
|
||||
public SAFFSNode createDirectory(SAFFSNode node, String name) {
|
||||
return createDocument(node, name, DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public SAFFSNode createDirectory(long nodeId, String name) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return createDirectory(node, name);
|
||||
}
|
||||
|
||||
public SAFFSNode createFile(SAFFSNode node, String name) {
|
||||
return createDocument(node, name, "application/octet-stream");
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public SAFFSNode createFile(long nodeId, String name) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return createFile(node, name);
|
||||
}
|
||||
|
||||
public int createReadStream(SAFFSNode node) {
|
||||
return createStream(node, "r");
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public int createReadStream(long nodeId) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return createReadStream(node);
|
||||
}
|
||||
|
||||
public int createWriteStream(SAFFSNode node) {
|
||||
return createStream(node, "wt");
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public int createWriteStream(long nodeId) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return createWriteStream(node);
|
||||
}
|
||||
|
||||
public int removeNode(SAFFSNode node) {
|
||||
final ContentResolver resolver = _context.getContentResolver();
|
||||
final Uri uri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
|
||||
|
||||
if ((node._flags & SAFFSNode.REMOVABLE) != 0) {
|
||||
final Uri parentUri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._parent._documentId);
|
||||
|
||||
long startIO = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
if (!DocumentsContract.removeDocument(resolver, uri, parentUri)) {
|
||||
return OsConstants.EIO;
|
||||
}
|
||||
} catch(FileNotFoundException e) {
|
||||
return OsConstants.ENOENT;
|
||||
} finally {
|
||||
long endIO = System.currentTimeMillis();
|
||||
reportIO(startIO, endIO);
|
||||
}
|
||||
} else if ((node._flags & SAFFSNode.DELETABLE) != 0) {
|
||||
long startIO = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
if (!DocumentsContract.deleteDocument(resolver, uri)) {
|
||||
return OsConstants.EIO;
|
||||
}
|
||||
} catch(FileNotFoundException e) {
|
||||
return OsConstants.ENOENT;
|
||||
} finally {
|
||||
long endIO = System.currentTimeMillis();
|
||||
reportIO(startIO, endIO);
|
||||
}
|
||||
} else {
|
||||
return OsConstants.EPERM;
|
||||
}
|
||||
|
||||
// Cleanup node
|
||||
node._parent._dirty = true;
|
||||
node.reset(null, null, null, 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public int removeNode(long nodeId) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return removeNode(node);
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public void removeTree() {
|
||||
final ContentResolver resolver = _context.getContentResolver();
|
||||
|
||||
String treeId = getTreeId();
|
||||
|
||||
resolver.releasePersistableUriPermission(_treeUri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
|
||||
if (_trees == null || _trees.remove(treeId) == null) {
|
||||
loadSAFTrees(_context);
|
||||
}
|
||||
}
|
||||
|
||||
private SAFFSNode createDocument(SAFFSNode node, String name, String mimeType) {
|
||||
final ContentResolver resolver = _context.getContentResolver();
|
||||
final Uri parentUri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
|
||||
Uri newDocUri;
|
||||
|
||||
// Make sure _children is OK
|
||||
if (node._children == null || node._dirty) {
|
||||
try {
|
||||
fetchChildren(node);
|
||||
} catch (Exception e) {
|
||||
Log.w(ScummVM.LOG_TAG, "Failed to get children: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
long startIO = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
newDocUri = DocumentsContract.createDocument(resolver, parentUri, mimeType, name);
|
||||
} catch(FileNotFoundException e) {
|
||||
return null;
|
||||
} finally {
|
||||
long endIO = System.currentTimeMillis();
|
||||
reportIO(startIO, endIO);
|
||||
}
|
||||
if (newDocUri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String documentId = DocumentsContract.getDocumentId(newDocUri);
|
||||
|
||||
SAFFSNode newnode = null;
|
||||
|
||||
SoftReference<SAFFSNode> oldnodeRef = node._children.remove(name);
|
||||
if (oldnodeRef != null) {
|
||||
newnode = oldnodeRef.get();
|
||||
}
|
||||
if (newnode == null) {
|
||||
newnode = new SAFFSNode();
|
||||
}
|
||||
|
||||
newnode.reset(node, node._path + "/" + name, documentId, 0);
|
||||
// Update flags
|
||||
final String realName = stat(newnode);
|
||||
if (realName == null) {
|
||||
return null;
|
||||
}
|
||||
// Unlikely but...
|
||||
if (!realName.equals(name)) {
|
||||
node._children.remove(realName);
|
||||
newnode._path = node._path + "/" + realName;
|
||||
}
|
||||
|
||||
node._children.put(realName, new SoftReference<>(newnode));
|
||||
|
||||
return newnode;
|
||||
}
|
||||
|
||||
public ParcelFileDescriptor createFileDescriptor(SAFFSNode node, String mode) {
|
||||
final ContentResolver resolver = _context.getContentResolver();
|
||||
final Uri uri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
|
||||
|
||||
ParcelFileDescriptor pfd;
|
||||
|
||||
long startIO = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
pfd = resolver.openFileDescriptor(uri, mode);
|
||||
} catch(FileNotFoundException e) {
|
||||
return null;
|
||||
} finally {
|
||||
long endIO = System.currentTimeMillis();
|
||||
reportIO(startIO, endIO);
|
||||
}
|
||||
|
||||
return pfd;
|
||||
}
|
||||
|
||||
private int createStream(SAFFSNode node, String mode) {
|
||||
ParcelFileDescriptor pfd = createFileDescriptor(node, mode);
|
||||
if (pfd == null) {
|
||||
return -1;
|
||||
}
|
||||
return pfd.detachFd();
|
||||
}
|
||||
|
||||
private String stat(SAFFSNode node) {
|
||||
final ContentResolver resolver = _context.getContentResolver();
|
||||
final Uri uri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
|
||||
|
||||
Cursor c = null;
|
||||
|
||||
long startIO = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
c = resolver.query(uri, new String[] { DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_FLAGS }, null, null, null);
|
||||
if (c == null) {
|
||||
return null;
|
||||
}
|
||||
if (c.moveToNext()) {
|
||||
final String displayName = c.getString(0);
|
||||
final String mimeType = c.getString(1);
|
||||
final int flags = c.getInt(2);
|
||||
|
||||
node._flags = SAFFSNode.computeFlags(mimeType, flags);
|
||||
|
||||
return displayName;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(ScummVM.LOG_TAG, "Failed query: " + e);
|
||||
} finally {
|
||||
if (c != null) {
|
||||
try {
|
||||
c.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
long endIO = System.currentTimeMillis();
|
||||
reportIO(startIO, endIO);
|
||||
}
|
||||
// We should never end up here
|
||||
// If we do, a tree or a file got likely removed
|
||||
return null;
|
||||
}
|
||||
}
|
||||
573
backends/platform/android/org/scummvm/scummvm/ScummVM.java
Normal file
573
backends/platform/android/org/scummvm/scummvm/ScummVM.java
Normal file
@@ -0,0 +1,573 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
import android.util.Log;
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Scanner;
|
||||
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.egl.EGLContext;
|
||||
import javax.microedition.khronos.egl.EGLDisplay;
|
||||
import javax.microedition.khronos.egl.EGLSurface;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
public abstract class ScummVM implements SurfaceHolder.Callback,
|
||||
CompatHelpers.SystemInsets.SystemInsetsListener, Runnable {
|
||||
public static final int SHOW_ON_SCREEN_MENU = 1;
|
||||
public static final int SHOW_ON_SCREEN_INPUT_MODE = 2;
|
||||
|
||||
final protected static String LOG_TAG = "ScummVM";
|
||||
final private AssetManager _asset_manager;
|
||||
final private Object _sem_surface;
|
||||
final private MyScummVMDestroyedCallback _svm_destroyed_callback;
|
||||
|
||||
private EGL10 _egl;
|
||||
private EGLDisplay _egl_display = EGL10.EGL_NO_DISPLAY;
|
||||
private EGLConfig _egl_config;
|
||||
private EGLContext _egl_context = EGL10.EGL_NO_CONTEXT;
|
||||
private EGLSurface _egl_surface = EGL10.EGL_NO_SURFACE;
|
||||
|
||||
private SurfaceHolder _surface_holder;
|
||||
private int bitsPerPixel;
|
||||
private AudioTrack _audio_track;
|
||||
private int _sample_rate = 0;
|
||||
private int _buffer_size = 0;
|
||||
|
||||
private boolean _assetsUpdated;
|
||||
private String[] _args;
|
||||
|
||||
private native void create(AssetManager asset_manager,
|
||||
EGL10 egl,
|
||||
EGLDisplay egl_display,
|
||||
AudioTrack audio_track,
|
||||
int sample_rate,
|
||||
int buffer_size,
|
||||
boolean assetsUpdated);
|
||||
private native void destroy();
|
||||
private native void setSurface(int width, int height, int bpp);
|
||||
private native int main(String[] args);
|
||||
|
||||
// pause the engine and all native threads
|
||||
final public native void setPause(boolean pause);
|
||||
// Feed an event to ScummVM. Safe to call from other threads.
|
||||
final public native void pushEvent(int type, int arg1, int arg2, int arg3,
|
||||
int arg4, int arg5, int arg6);
|
||||
// Update the 3D touch controls
|
||||
final public native void setupTouchMode(int oldValue, int newValue);
|
||||
final public native void updateTouch(int action, int ptr, int x, int y);
|
||||
|
||||
final public native void syncVirtkeyboardState(boolean newState);
|
||||
|
||||
final public native String getNativeVersionInfo();
|
||||
|
||||
// CompatHelpers.WindowInsets.SystemInsetsListener interface
|
||||
@Override
|
||||
final public native void systemInsetsUpdated(int[] gestureInsets, int[] systemInsets, int[] cutoutInsets);
|
||||
|
||||
// Callbacks from C++ peer instance
|
||||
abstract protected void getDPI(float[] values);
|
||||
abstract protected void displayMessageOnOSD(String msg);
|
||||
abstract protected void openUrl(String url);
|
||||
abstract protected boolean hasTextInClipboard();
|
||||
abstract protected String getTextFromClipboard();
|
||||
abstract protected boolean setTextInClipboard(String text);
|
||||
abstract protected boolean isConnectionLimited();
|
||||
abstract protected void setWindowCaption(String caption);
|
||||
abstract protected void showVirtualKeyboard(boolean enable);
|
||||
abstract protected void showOnScreenControls(int enableMask);
|
||||
abstract protected void setTouchMode(int touchMode);
|
||||
abstract protected int getTouchMode();
|
||||
abstract protected void setOrientation(int orientation);
|
||||
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();
|
||||
abstract protected SAFFSTree getNewSAFTree(boolean write, String initialURI, String prompt);
|
||||
abstract protected SAFFSTree[] getSAFTrees();
|
||||
abstract protected SAFFSTree findSAFTree(String name);
|
||||
abstract protected int exportBackup(String prompt);
|
||||
abstract protected int importBackup(String prompt, String path);
|
||||
|
||||
public ScummVM(AssetManager asset_manager, SurfaceHolder holder, final MyScummVMDestroyedCallback scummVMDestroyedCallback) {
|
||||
_asset_manager = asset_manager;
|
||||
_sem_surface = new Object();
|
||||
_svm_destroyed_callback = scummVMDestroyedCallback;
|
||||
holder.addCallback(this);
|
||||
}
|
||||
|
||||
final public String getInstallingScummVMVersionInfo() {
|
||||
return getNativeVersionInfo();
|
||||
}
|
||||
|
||||
// SurfaceHolder callback
|
||||
final public void surfaceCreated(SurfaceHolder holder) {
|
||||
Log.d(LOG_TAG, "surfaceCreated");
|
||||
|
||||
// no need to do anything, surfaceChanged() will be called in any case
|
||||
}
|
||||
|
||||
// SurfaceHolder callback
|
||||
final public void surfaceChanged(SurfaceHolder holder, int format,
|
||||
int width, int height) {
|
||||
|
||||
PixelFormat pixelFormat = new PixelFormat();
|
||||
PixelFormat.getPixelFormatInfo(format, pixelFormat);
|
||||
bitsPerPixel = pixelFormat.bitsPerPixel;
|
||||
|
||||
Log.d(LOG_TAG, String.format(Locale.ROOT, "surfaceChanged: %dx%d (%d: %dbpp)",
|
||||
width, height, format, bitsPerPixel));
|
||||
|
||||
// store values for the native code
|
||||
// make sure to do it before notifying the lock
|
||||
// as it leads to a race condition otherwise
|
||||
setSurface(width, height, bitsPerPixel);
|
||||
|
||||
synchronized(_sem_surface) {
|
||||
_surface_holder = holder;
|
||||
_sem_surface.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
// SurfaceHolder callback
|
||||
final public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
Log.d(LOG_TAG, "surfaceDestroyed");
|
||||
|
||||
synchronized(_sem_surface) {
|
||||
_surface_holder = null;
|
||||
_sem_surface.notifyAll();
|
||||
}
|
||||
|
||||
// Don't call when EGL is not init:
|
||||
// this avoids polluting the static variables with obsolete values
|
||||
if (_egl != null) {
|
||||
// clear values for the native code
|
||||
setSurface(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
final public void setAssetsUpdated(boolean assetsUpdated) {
|
||||
_assetsUpdated = assetsUpdated;
|
||||
}
|
||||
|
||||
final public void setArgs(String[] args) {
|
||||
_args = args;
|
||||
}
|
||||
|
||||
final public void run() {
|
||||
try {
|
||||
// wait for the surfaceChanged callback
|
||||
synchronized(_sem_surface) {
|
||||
while (_surface_holder == null)
|
||||
_sem_surface.wait();
|
||||
}
|
||||
|
||||
initAudio();
|
||||
initEGL();
|
||||
} catch (Exception e) {
|
||||
deinitEGL();
|
||||
deinitAudio();
|
||||
|
||||
throw new RuntimeException("Error preparing the ScummVM thread", e);
|
||||
}
|
||||
|
||||
create(_asset_manager, _egl, _egl_display,
|
||||
_audio_track, _sample_rate, _buffer_size,
|
||||
_assetsUpdated);
|
||||
|
||||
int res = main(_args);
|
||||
|
||||
destroy();
|
||||
|
||||
deinitEGL();
|
||||
deinitAudio();
|
||||
|
||||
// Don't exit force-ably here!
|
||||
if (_svm_destroyed_callback != null) {
|
||||
_svm_destroyed_callback.handle(res);
|
||||
}
|
||||
}
|
||||
|
||||
private void initEGL() throws Exception {
|
||||
_egl = (EGL10)EGLContext.getEGL();
|
||||
_egl_display = _egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
|
||||
|
||||
int[] version = new int[2];
|
||||
_egl.eglInitialize(_egl_display, version);
|
||||
Log.d(LOG_TAG, String.format(Locale.ROOT, "EGL version %d.%d initialized", version[0], version[1]));
|
||||
|
||||
int[] num_config = new int[1];
|
||||
_egl.eglGetConfigs(_egl_display, null, 0, num_config);
|
||||
|
||||
final int numConfigs = num_config[0];
|
||||
|
||||
if (numConfigs <= 0)
|
||||
throw new IllegalArgumentException("No EGL configs");
|
||||
|
||||
EGLConfig[] configs = new EGLConfig[numConfigs];
|
||||
_egl.eglGetConfigs(_egl_display, configs, numConfigs, num_config);
|
||||
|
||||
// Android's eglChooseConfig is busted in several versions and
|
||||
// devices so we have to filter/rank the configs ourselves.
|
||||
_egl_config = chooseEglConfig(configs, version);
|
||||
|
||||
int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
|
||||
int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 2,
|
||||
EGL10.EGL_NONE };
|
||||
_egl_context = _egl.eglCreateContext(_egl_display, _egl_config,
|
||||
EGL10.EGL_NO_CONTEXT, attrib_list);
|
||||
|
||||
if (_egl_context == EGL10.EGL_NO_CONTEXT)
|
||||
throw new Exception(String.format(Locale.ROOT, "Failed to create context: 0x%x",
|
||||
_egl.eglGetError()));
|
||||
}
|
||||
|
||||
// Callback from C++ peer instance
|
||||
final protected EGLSurface initSurface() throws Exception {
|
||||
_egl_surface = _egl.eglCreateWindowSurface(_egl_display, _egl_config,
|
||||
_surface_holder, null);
|
||||
|
||||
if (_egl_surface == EGL10.EGL_NO_SURFACE)
|
||||
throw new Exception(String.format(Locale.ROOT,
|
||||
"eglCreateWindowSurface failed: 0x%x", _egl.eglGetError()));
|
||||
|
||||
_egl.eglMakeCurrent(_egl_display, _egl_surface, _egl_surface,
|
||||
_egl_context);
|
||||
|
||||
GL10 gl = (GL10)_egl_context.getGL();
|
||||
|
||||
Log.i(LOG_TAG, String.format(Locale.ROOT, "Using EGL %s (%s); GL %s/%s (%s)",
|
||||
_egl.eglQueryString(_egl_display, EGL10.EGL_VERSION),
|
||||
_egl.eglQueryString(_egl_display, EGL10.EGL_VENDOR),
|
||||
gl.glGetString(GL10.GL_VERSION),
|
||||
gl.glGetString(GL10.GL_RENDERER),
|
||||
gl.glGetString(GL10.GL_VENDOR)));
|
||||
|
||||
return _egl_surface;
|
||||
}
|
||||
|
||||
// Callback from C++ peer instance
|
||||
final protected void deinitSurface() {
|
||||
if (_egl_display != EGL10.EGL_NO_DISPLAY) {
|
||||
_egl.eglMakeCurrent(_egl_display, EGL10.EGL_NO_SURFACE,
|
||||
EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
|
||||
|
||||
if (_egl_surface != EGL10.EGL_NO_SURFACE)
|
||||
_egl.eglDestroySurface(_egl_display, _egl_surface);
|
||||
}
|
||||
|
||||
_egl_surface = EGL10.EGL_NO_SURFACE;
|
||||
}
|
||||
|
||||
// Callback from C++ peer instance
|
||||
final protected int eglVersion() {
|
||||
String version = _egl.eglQueryString(_egl_display, EGL10.EGL_VERSION);
|
||||
if (version == null) {
|
||||
// 1.0
|
||||
return 0x00010000;
|
||||
}
|
||||
|
||||
Scanner versionScan = new Scanner(version).useLocale(Locale.ROOT).useDelimiter("[ .]");
|
||||
int versionInt = versionScan.nextInt() << 16;
|
||||
versionInt |= versionScan.nextInt() & 0xffff;
|
||||
return versionInt;
|
||||
}
|
||||
|
||||
private void deinitEGL() {
|
||||
if (_egl_display != EGL10.EGL_NO_DISPLAY) {
|
||||
_egl.eglMakeCurrent(_egl_display, EGL10.EGL_NO_SURFACE,
|
||||
EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
|
||||
|
||||
if (_egl_surface != EGL10.EGL_NO_SURFACE)
|
||||
_egl.eglDestroySurface(_egl_display, _egl_surface);
|
||||
|
||||
if (_egl_context != EGL10.EGL_NO_CONTEXT)
|
||||
_egl.eglDestroyContext(_egl_display, _egl_context);
|
||||
|
||||
_egl.eglTerminate(_egl_display);
|
||||
}
|
||||
|
||||
_egl_surface = EGL10.EGL_NO_SURFACE;
|
||||
_egl_context = EGL10.EGL_NO_CONTEXT;
|
||||
_egl_config = null;
|
||||
_egl_display = EGL10.EGL_NO_DISPLAY;
|
||||
_egl = null;
|
||||
}
|
||||
|
||||
private void initAudio() throws Exception {
|
||||
_sample_rate = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);
|
||||
_buffer_size = AudioTrack.getMinBufferSize(_sample_rate,
|
||||
AudioFormat.CHANNEL_OUT_STEREO,
|
||||
AudioFormat.ENCODING_PCM_16BIT);
|
||||
|
||||
// ~50ms
|
||||
int buffer_size_want = (_sample_rate * 2 * 2 / 20) & ~1023;
|
||||
|
||||
if (_buffer_size < buffer_size_want) {
|
||||
Log.w(LOG_TAG, String.format(Locale.ROOT,
|
||||
"adjusting audio buffer size (was: %d)", _buffer_size));
|
||||
|
||||
_buffer_size = buffer_size_want;
|
||||
}
|
||||
|
||||
Log.i(LOG_TAG, String.format(Locale.ROOT, "Using %d bytes buffer for %dHz audio",
|
||||
_buffer_size, _sample_rate));
|
||||
|
||||
CompatHelpers.AudioTrackCompat.AudioTrackCompatReturn audioTrackRet =
|
||||
CompatHelpers.AudioTrackCompat.make(_sample_rate, _buffer_size);
|
||||
_audio_track = audioTrackRet.audioTrack;
|
||||
_buffer_size = audioTrackRet.bufferSize;
|
||||
|
||||
if (_audio_track.getState() != AudioTrack.STATE_INITIALIZED)
|
||||
throw new Exception(
|
||||
String.format(Locale.ROOT, "Error initializing AudioTrack: %d",
|
||||
_audio_track.getState()));
|
||||
}
|
||||
|
||||
private void deinitAudio() {
|
||||
if (_audio_track != null)
|
||||
_audio_track.release();
|
||||
|
||||
_audio_track = null;
|
||||
_buffer_size = 0;
|
||||
_sample_rate = 0;
|
||||
}
|
||||
|
||||
private static final int[] s_eglAttribs = {
|
||||
EGL10.EGL_CONFIG_ID,
|
||||
EGL10.EGL_BUFFER_SIZE,
|
||||
EGL10.EGL_RED_SIZE,
|
||||
EGL10.EGL_GREEN_SIZE,
|
||||
EGL10.EGL_BLUE_SIZE,
|
||||
EGL10.EGL_ALPHA_SIZE,
|
||||
EGL10.EGL_CONFIG_CAVEAT,
|
||||
EGL10.EGL_DEPTH_SIZE,
|
||||
EGL10.EGL_LEVEL,
|
||||
EGL10.EGL_MAX_PBUFFER_WIDTH,
|
||||
EGL10.EGL_MAX_PBUFFER_HEIGHT,
|
||||
EGL10.EGL_MAX_PBUFFER_PIXELS,
|
||||
EGL10.EGL_NATIVE_RENDERABLE,
|
||||
EGL10.EGL_NATIVE_VISUAL_ID,
|
||||
EGL10.EGL_NATIVE_VISUAL_TYPE,
|
||||
EGL10.EGL_SAMPLE_BUFFERS,
|
||||
EGL10.EGL_SAMPLES,
|
||||
EGL10.EGL_STENCIL_SIZE,
|
||||
EGL10.EGL_SURFACE_TYPE,
|
||||
EGL10.EGL_TRANSPARENT_TYPE,
|
||||
EGL10.EGL_TRANSPARENT_RED_VALUE,
|
||||
EGL10.EGL_TRANSPARENT_GREEN_VALUE,
|
||||
EGL10.EGL_TRANSPARENT_BLUE_VALUE,
|
||||
EGL10.EGL_RENDERABLE_TYPE
|
||||
};
|
||||
final private static int EGL_OPENGL_ES_BIT = 1;
|
||||
final private static int EGL_OPENGL_ES2_BIT = 4;
|
||||
|
||||
final private class EglAttribs {
|
||||
|
||||
LinkedHashMap<Integer, Integer> _lhm;
|
||||
|
||||
public EglAttribs(EGLConfig config) {
|
||||
_lhm = new LinkedHashMap<>(s_eglAttribs.length);
|
||||
|
||||
int[] value = new int[1];
|
||||
|
||||
// prevent throwing IllegalArgumentException
|
||||
if (_egl_display == null || config == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i : s_eglAttribs) {
|
||||
_egl.eglGetConfigAttrib(_egl_display, config, i, value);
|
||||
|
||||
_lhm.put(i, value[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private int weightBits(int attr, int size) {
|
||||
final int value = get(attr);
|
||||
|
||||
int score = 0;
|
||||
|
||||
if (value == size || (size > 0 && value > size))
|
||||
score += 10;
|
||||
|
||||
// penalize for wasted bits
|
||||
if (value > size)
|
||||
score -= value - size;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
public int weight() {
|
||||
int score = 10000;
|
||||
|
||||
if (get(EGL10.EGL_CONFIG_CAVEAT) != EGL10.EGL_NONE)
|
||||
score -= 1000;
|
||||
|
||||
// If there is a config with EGL_OPENGL_ES2_BIT it must be favored
|
||||
// This attribute can only be checked with EGL 1.3 but it may be present on older versions
|
||||
if ((get(EGL10.EGL_RENDERABLE_TYPE) & EGL_OPENGL_ES2_BIT) > 0)
|
||||
score += 5000;
|
||||
|
||||
// less MSAA is better
|
||||
score -= get(EGL10.EGL_SAMPLES) * 100;
|
||||
|
||||
// Must be at least 565, but then smaller is better
|
||||
score += weightBits(EGL10.EGL_RED_SIZE, 5);
|
||||
score += weightBits(EGL10.EGL_GREEN_SIZE, 6);
|
||||
score += weightBits(EGL10.EGL_BLUE_SIZE, 5);
|
||||
score += weightBits(EGL10.EGL_ALPHA_SIZE, 0);
|
||||
// Prefer 24 bits depth
|
||||
score += weightBits(EGL10.EGL_DEPTH_SIZE, 24);
|
||||
score += weightBits(EGL10.EGL_STENCIL_SIZE, 8);
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String toString() {
|
||||
String s;
|
||||
|
||||
if (get(EGL10.EGL_ALPHA_SIZE) > 0)
|
||||
s = String.format(Locale.ROOT, "[%d] RGBA%d%d%d%d",
|
||||
get(EGL10.EGL_CONFIG_ID),
|
||||
get(EGL10.EGL_RED_SIZE),
|
||||
get(EGL10.EGL_GREEN_SIZE),
|
||||
get(EGL10.EGL_BLUE_SIZE),
|
||||
get(EGL10.EGL_ALPHA_SIZE));
|
||||
else
|
||||
s = String.format(Locale.ROOT, "[%d] RGB%d%d%d",
|
||||
get(EGL10.EGL_CONFIG_ID),
|
||||
get(EGL10.EGL_RED_SIZE),
|
||||
get(EGL10.EGL_GREEN_SIZE),
|
||||
get(EGL10.EGL_BLUE_SIZE));
|
||||
|
||||
if (get(EGL10.EGL_DEPTH_SIZE) > 0)
|
||||
s += String.format(Locale.ROOT, " D%d", get(EGL10.EGL_DEPTH_SIZE));
|
||||
|
||||
if (get(EGL10.EGL_STENCIL_SIZE) > 0)
|
||||
s += String.format(Locale.ROOT, " S%d", get(EGL10.EGL_STENCIL_SIZE));
|
||||
|
||||
if (get(EGL10.EGL_SAMPLES) > 0)
|
||||
s += String.format(Locale.ROOT, " MSAAx%d", get(EGL10.EGL_SAMPLES));
|
||||
|
||||
if ((get(EGL10.EGL_SURFACE_TYPE) & EGL10.EGL_WINDOW_BIT) > 0)
|
||||
s += " W";
|
||||
if ((get(EGL10.EGL_SURFACE_TYPE) & EGL10.EGL_PBUFFER_BIT) > 0)
|
||||
s += " P";
|
||||
if ((get(EGL10.EGL_SURFACE_TYPE) & EGL10.EGL_PIXMAP_BIT) > 0)
|
||||
s += " X";
|
||||
|
||||
if ((get(EGL10.EGL_RENDERABLE_TYPE) & EGL_OPENGL_ES_BIT) > 0)
|
||||
s += " ES";
|
||||
if ((get(EGL10.EGL_RENDERABLE_TYPE) & EGL_OPENGL_ES2_BIT) > 0)
|
||||
s += " ES2";
|
||||
|
||||
|
||||
switch (get(EGL10.EGL_CONFIG_CAVEAT)) {
|
||||
case EGL10.EGL_NONE:
|
||||
break;
|
||||
|
||||
case EGL10.EGL_SLOW_CONFIG:
|
||||
s += " SLOW";
|
||||
break;
|
||||
|
||||
case EGL10.EGL_NON_CONFORMANT_CONFIG:
|
||||
s += " NON_CONFORMANT";
|
||||
|
||||
default:
|
||||
s += String.format(Locale.ROOT, " unknown CAVEAT 0x%x",
|
||||
get(EGL10.EGL_CONFIG_CAVEAT));
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
public Integer get(Integer key) {
|
||||
if (_lhm.containsKey(key) && _lhm.get(key) != null) {
|
||||
return _lhm.get(key);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private EGLConfig chooseEglConfig(EGLConfig[] configs, int[] version) {
|
||||
EGLConfig res = configs[0];
|
||||
int bestScore = -1;
|
||||
|
||||
Log.d(LOG_TAG, "EGL configs:");
|
||||
|
||||
for (EGLConfig config : configs) {
|
||||
if (config != null) {
|
||||
boolean good = true;
|
||||
|
||||
EglAttribs attr = new EglAttribs(config);
|
||||
|
||||
// must have
|
||||
if ((attr.get(EGL10.EGL_SURFACE_TYPE) & EGL10.EGL_WINDOW_BIT) == 0)
|
||||
good = false;
|
||||
|
||||
if (version[0] >= 2 ||
|
||||
(version[0] == 1 && version[1] >= 3)) {
|
||||
// EGL_OPENGL_ES2_BIT is only supported since EGL 1.3
|
||||
if ((attr.get(EGL10.EGL_RENDERABLE_TYPE) & EGL_OPENGL_ES2_BIT) == 0)
|
||||
good = false;
|
||||
}
|
||||
if (attr.get(EGL10.EGL_BUFFER_SIZE) < bitsPerPixel)
|
||||
good = false;
|
||||
|
||||
// Force a config with a depth buffer and a stencil buffer when rendering directly on backbuffer
|
||||
if ((attr.get(EGL10.EGL_DEPTH_SIZE) == 0) || (attr.get(EGL10.EGL_STENCIL_SIZE) == 0))
|
||||
good = false;
|
||||
|
||||
int score = attr.weight();
|
||||
|
||||
Log.d(LOG_TAG, String.format(Locale.ROOT, "%s (%d, %s)", attr.toString(), score, good ? "OK" : "NOK"));
|
||||
|
||||
if (!good) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (score > bestScore) {
|
||||
res = config;
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestScore < 0)
|
||||
Log.e(LOG_TAG,
|
||||
"Unable to find an acceptable EGL config, expect badness.");
|
||||
|
||||
Log.d(LOG_TAG, String.format(Locale.ROOT, "Chosen EGL config: %s",
|
||||
new EglAttribs(res).toString()));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static {
|
||||
// // For grabbing with gdb...
|
||||
// final boolean sleep_for_debugger = false;
|
||||
// if (sleep_for_debugger) {
|
||||
// try {
|
||||
// Thread.sleep(20 * 1000);
|
||||
// } catch (InterruptedException ignored) {
|
||||
// }
|
||||
// }
|
||||
|
||||
System.loadLibrary("scummvm");
|
||||
}
|
||||
}
|
||||
2404
backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
Normal file
2404
backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
Normal file
File diff suppressed because it is too large
Load Diff
1035
backends/platform/android/org/scummvm/scummvm/ScummVMEvents.java
Normal file
1035
backends/platform/android/org/scummvm/scummvm/ScummVMEvents.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,488 @@
|
||||
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.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;
|
||||
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.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
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.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class ShortcutCreatorActivity extends Activity implements CompatHelpers.SystemInsets.SystemInsetsListener {
|
||||
final protected static String LOG_TAG = "ShortcutCreatorActivity";
|
||||
|
||||
private IconsCache _cache;
|
||||
|
||||
static void pushShortcut(Context context, String gameId, Intent intent) {
|
||||
Map<String, Map<String, String>> parsedIniMap;
|
||||
try (FileReader reader = new FileReader(new File(context.getFilesDir(), "scummvm.ini"))) {
|
||||
parsedIniMap = INIParser.parse(reader);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
parsedIniMap = null;
|
||||
} catch(IOException ignored) {
|
||||
parsedIniMap = null;
|
||||
}
|
||||
|
||||
if (parsedIniMap == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Game game = Game.loadGame(parsedIniMap, gameId);
|
||||
if (game == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileInputStream defaultStream = openFile(new File(context.getFilesDir(), "gui-icons.dat"));
|
||||
|
||||
File iconsPath = INIParser.getPath(parsedIniMap, "scummvm", "iconspath",
|
||||
new File(context.getFilesDir(), "icons"));
|
||||
FileInputStream[] packsStream = openFiles(context, iconsPath, "gui-icons.*\\.dat");
|
||||
|
||||
IconsCache cache = new IconsCache(context, defaultStream, packsStream);
|
||||
final Drawable icon = cache.getGameIcon(game);
|
||||
|
||||
CompatHelpers.ShortcutCreator.pushDynamicShortcut(context, game.getTarget(), intent, game.getDescription(), icon, R.drawable.ic_no_game_icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.shortcut_creator_activity);
|
||||
|
||||
CompatHelpers.SystemInsets.registerSystemInsetsListener(findViewById(R.id.shortcut_creator_root), this);
|
||||
|
||||
// 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);
|
||||
|
||||
FileInputStream defaultStream = openFile(new File(getFilesDir(), "gui-icons.dat"));
|
||||
|
||||
File iconsPath = INIParser.getPath(parsedIniMap, "scummvm", "iconspath",
|
||||
new File(getFilesDir(), "icons"));
|
||||
FileInputStream[] packsStream = openFiles(this, iconsPath, "gui-icons.*\\.dat");
|
||||
|
||||
_cache = new IconsCache(this, defaultStream, packsStream);
|
||||
|
||||
final GameAdapter 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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void systemInsetsUpdated(int[] gestureInsets, int[] systemInsets, int[] cutoutInsets) {
|
||||
LinearLayout root = findViewById(R.id.shortcut_creator_root);
|
||||
// Ignore bottom as we have our list which can overflow
|
||||
root.setPadding(
|
||||
Math.max(systemInsets[0], cutoutInsets[0]),
|
||||
Math.max(systemInsets[1], cutoutInsets[1]),
|
||||
Math.max(systemInsets[2], cutoutInsets[2]), 0);
|
||||
}
|
||||
|
||||
static private FileInputStream openFile(File path) {
|
||||
try {
|
||||
return new FileInputStream(path);
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static private FileInputStream[] openFiles(Context context, File basePath, String regex) {
|
||||
SAFFSTree.PathResult pr;
|
||||
try {
|
||||
pr = SAFFSTree.fullPathToNode(context, basePath.getPath(), false);
|
||||
} catch (FileNotFoundException e) {
|
||||
return new FileInputStream[0];
|
||||
}
|
||||
|
||||
// This version check is only to make Android Studio linter happy
|
||||
if (pr == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
// This is a standard filesystem path
|
||||
File[] children = basePath.listFiles((dir, name) -> name.matches(regex));
|
||||
if (children == null) {
|
||||
return new FileInputStream[0];
|
||||
}
|
||||
Arrays.sort(children);
|
||||
FileInputStream[] ret = new FileInputStream[children.length];
|
||||
int i = 0;
|
||||
for (File f: children) {
|
||||
ret[i] = openFile(f);
|
||||
i += 1;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// This is a SAF fake mount point
|
||||
SAFFSTree.SAFFSNode[] children = pr.tree.getChildren(pr.node);
|
||||
if (children == null) {
|
||||
return new FileInputStream[0];
|
||||
}
|
||||
Arrays.sort(children);
|
||||
|
||||
ArrayList<FileInputStream> 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 = pr.tree.createFileDescriptor(child, "r");
|
||||
if (pfd == null) {
|
||||
continue;
|
||||
}
|
||||
ret.add(new ParcelFileDescriptor.AutoCloseInputStream(pfd));
|
||||
}
|
||||
return ret.toArray(new FileInputStream[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();
|
||||
|
||||
String label = desc.getText().toString();
|
||||
// Generate an id which depends on the user description
|
||||
// Without this, if the user changes the description but already has the same shortcut (also in the dynamic ones), the other label will be reused
|
||||
String shortcutId = game.getTarget() + String.format("-%08x", label.hashCode());
|
||||
|
||||
Intent shortcut = new Intent(Intent.ACTION_MAIN, Uri.fromParts("scummvm", game.getTarget(), null),
|
||||
ShortcutCreatorActivity.this, SplashActivity.class);
|
||||
Intent result = CompatHelpers.ShortcutCreator.createShortcutResultIntent(ShortcutCreatorActivity.this, shortcutId, 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 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 Game loadGame(@NonNull Map<String, Map<String, String>> parsedIniMap, String target) {
|
||||
Map<String, String> domain = parsedIniMap.get(target);
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
String engineid = domain.get("engineid");
|
||||
String gameid = domain.get("gameid");
|
||||
String description = domain.get("description");
|
||||
if (description == null) {
|
||||
return null;
|
||||
}
|
||||
return new Game(target, engineid, gameid, description);
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
private final Context _context;
|
||||
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];
|
||||
|
||||
private final List<ZipFile> _zipFiles = new ArrayList<>();
|
||||
|
||||
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;
|
||||
}
|
||||
try {
|
||||
ZipFile zf = new ZipFile(packStream);
|
||||
_zipFiles.add(zf);
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Error while loading pack ZipFile: " + i, e);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable getGameIcon(Game game) {
|
||||
for (String name : game.getIconCandidates()) {
|
||||
byte[] data = _icons.get(name);
|
||||
if (data == null) {
|
||||
data = loadIcon(name);
|
||||
}
|
||||
if (data == _noIconSentinel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
|
||||
if (bitmap == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return new BitmapDrawable(_context.getResources(), bitmap);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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) ze.getSize();
|
||||
byte[] buffer = new byte[sz];
|
||||
|
||||
try (InputStream is = zf.getInputStream(ze)) {
|
||||
if (is.read(buffer) != buffer.length) {
|
||||
throw new IOException();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Error while uncompressing: " + name + " from zip file " + zfi, e);
|
||||
zfi++;
|
||||
continue;
|
||||
}
|
||||
|
||||
_icons.put(name, buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Register failure
|
||||
_icons.put(name, _noIconSentinel);
|
||||
return _noIconSentinel;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.Manifest;
|
||||
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.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SplashActivity extends Activity {
|
||||
|
||||
/**
|
||||
* Ids to identify an external storage read (and write) request.
|
||||
* They are app-defined int constants. The callback method gets the result of the request.
|
||||
* Ie. returned in the Activity's onRequestPermissionsResult()
|
||||
*/
|
||||
private static final int MY_PERMISSION_ALL = 110;
|
||||
|
||||
private static final String[] MY_PERMISSIONS_STR_LIST = {
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
};
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
&& (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
|
||||
|| checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
|
||||
) {
|
||||
// In Tiramisu (API 33) and above, READ and WRITE external storage permissions have no effect,
|
||||
// and they are automatically denied -- onRequestPermissionsResult() will be called without user's input
|
||||
requestPermissions(MY_PERMISSIONS_STR_LIST, MY_PERMISSION_ALL);
|
||||
} else {
|
||||
Intent next = new Intent(this, ScummVMActivity.class);
|
||||
next.fillIn(getIntent(), Intent.FILL_IN_ACTION | Intent.FILL_IN_DATA);
|
||||
startActivity(next);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
CompatHelpers.HideSystemStatusBar.hide(getWindow());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (requestCode == MY_PERMISSION_ALL) {
|
||||
int numOfReqPermsGranted = 0;
|
||||
// If request is canceled, the result arrays are empty.
|
||||
for (int i = 0; i < grantResults.length; ++i) {
|
||||
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i(ScummVM.LOG_TAG, permissions[i] + " permission was granted at Runtime");
|
||||
++numOfReqPermsGranted;
|
||||
} else {
|
||||
Log.i(ScummVM.LOG_TAG, permissions[i] + " permission was denied at Runtime");
|
||||
}
|
||||
}
|
||||
|
||||
if (numOfReqPermsGranted != grantResults.length) {
|
||||
// permission denied! We won't be able to make use of functionality depending on this permission.
|
||||
Toast.makeText(this, "Until permission is granted, some storage locations may be inaccessible for r/w!", Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
startActivity(new Intent(this, ScummVMActivity.class));
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
if (hasFocus) {
|
||||
CompatHelpers.HideSystemStatusBar.hide(getWindow());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
}
|
||||
|
||||
}
|
||||
74
backends/platform/android/org/scummvm/scummvm/Version.java
Normal file
74
backends/platform/android/org/scummvm/scummvm/Version.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
// Based on code from: https://stackoverflow.com/a/11024200
|
||||
public class Version implements Comparable<Version> {
|
||||
|
||||
private final String versionOnlyDigits;
|
||||
private final String versionDescription;
|
||||
|
||||
public final String getDescription() {
|
||||
return this.versionDescription;
|
||||
}
|
||||
|
||||
public final String get() {
|
||||
return this.versionOnlyDigits;
|
||||
}
|
||||
|
||||
public Version(String version) {
|
||||
if(version == null) {
|
||||
this.versionOnlyDigits = "0";
|
||||
this.versionDescription = "0";
|
||||
} else {
|
||||
this.versionDescription = version;
|
||||
// cleanup from any non-digit characters in the version string
|
||||
final String strippedVersion = version.replaceAll("[^\\d.]", "");
|
||||
if (!strippedVersion.matches("[0-9]+(\\.[0-9]+)*")) {
|
||||
this.versionOnlyDigits = "0";
|
||||
} else {
|
||||
this.versionOnlyDigits = strippedVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here a version is considered "dirty" if it contains other characters in the description string than the expected digits (and dots) of a "clean" proper version
|
||||
// eg. 2.3.0pre or 2.3.0git or 2.3.0git9272-gc71ac4748b are dirty
|
||||
// 2.3.0 is NOT dirty
|
||||
public boolean isDirty() {
|
||||
return (versionOnlyDigits.compareTo(versionDescription) != 0);
|
||||
}
|
||||
|
||||
|
||||
@Override public int compareTo(Version that) {
|
||||
if(that == null)
|
||||
return 1;
|
||||
String[] thisParts = this.get().split("\\.");
|
||||
String[] thatParts = that.get().split("\\.");
|
||||
int length = Math.max(thisParts.length, thatParts.length);
|
||||
try {
|
||||
for (int i = 0; i < length; i++) {
|
||||
int thisPart = i < thisParts.length ?
|
||||
Integer.parseInt(thisParts[i]) : 0;
|
||||
int thatPart = i < thatParts.length ?
|
||||
Integer.parseInt(thatParts[i]) : 0;
|
||||
if (thisPart < thatPart)
|
||||
return -1;
|
||||
if (thisPart > thatPart)
|
||||
return 1;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object that) {
|
||||
if(this == that)
|
||||
return true;
|
||||
if(that == null)
|
||||
return false;
|
||||
if(this.getClass() != that.getClass())
|
||||
return false;
|
||||
return this.compareTo((Version) that) == 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.scummvm.scummvm.net;
|
||||
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class HTTPManager {
|
||||
protected ExecutorService _executor;
|
||||
protected ArrayBlockingQueue<Runnable> _queue;
|
||||
protected boolean _empty;
|
||||
|
||||
/** @noinspection unused
|
||||
* Called from JNI (main ScummVM thread)
|
||||
*/
|
||||
public HTTPManager() {
|
||||
TLSSocketFactory.init();
|
||||
|
||||
_executor = Executors.newCachedThreadPool();
|
||||
// Use a capacity to make sure the queue is checked on a regular basis
|
||||
_queue = new ArrayBlockingQueue<>(50);
|
||||
_empty = true;
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* Called from JNI (main ScummVM thread)
|
||||
*/
|
||||
public void startRequest(HTTPRequest request) {
|
||||
request._manager = this;
|
||||
_executor.execute(request);
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* Called from JNI (main ScummVM thread)
|
||||
*/
|
||||
public void poll() {
|
||||
Runnable r;
|
||||
while((r = _queue.poll()) != null) {
|
||||
r.run();
|
||||
}
|
||||
// The read is never synchronized but at least we ensure here that we don't miss any event
|
||||
synchronized(this) {
|
||||
_empty = _queue.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
// Called from workers
|
||||
void enqueue(Runnable r) {
|
||||
while(true) {
|
||||
try {
|
||||
_queue.put(r);
|
||||
synchronized(this) {
|
||||
_empty = false;
|
||||
}
|
||||
return;
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package org.scummvm.scummvm.net;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import org.scummvm.scummvm.SAFFSTree;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.SequenceInputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.Vector;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class HTTPRequest implements Runnable {
|
||||
final static String LOG_TAG = "ScummVM.HTTP";
|
||||
private static final int DEFAULT_BUFFER_SIZE = 16384;
|
||||
|
||||
HTTPManager _manager;
|
||||
|
||||
protected String _method;
|
||||
protected AtomicReference<String> _url;
|
||||
protected TreeMap<String, String> _requestHeaders;
|
||||
protected InputStream _uploadBuffer;
|
||||
protected int _uploadBufferLength;
|
||||
|
||||
protected long _nativePointer;
|
||||
|
||||
protected AtomicBoolean _cancelled;
|
||||
|
||||
protected native void gotHeaders(long nativePointer, String[] headers);
|
||||
protected native void gotData(long nativePointer, byte[] data, int size, int totalSize);
|
||||
protected native void finished(long nativePointer, int errorCode, String errorMsg);
|
||||
|
||||
/** @noinspection unused
|
||||
* Called from JNI
|
||||
*/
|
||||
public HTTPRequest(long nativePointer, String url, String[] requestHeaders, byte[] uploadBuffer, boolean uploading, boolean usingPatch, boolean post) {
|
||||
init(nativePointer, url);
|
||||
setupUploadBuffer(uploadBuffer, uploading, usingPatch, post);
|
||||
setupHeaders(requestHeaders);
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* Called from JNI
|
||||
*/
|
||||
public HTTPRequest(long nativePointer, String url, String[] requestHeaders, String[] formFields, String[] formFiles) {
|
||||
init(nativePointer, url);
|
||||
setupMultipartForm(formFields, formFiles);
|
||||
setupHeaders(requestHeaders);
|
||||
}
|
||||
|
||||
private void init(long nativePointer, String url) {
|
||||
_nativePointer = nativePointer;
|
||||
_url = new AtomicReference<>(url);
|
||||
_requestHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
_cancelled = new AtomicBoolean(false);
|
||||
_method = "GET";
|
||||
}
|
||||
|
||||
private void setupUploadBuffer(byte[] uploadBuffer, boolean uploading, boolean usingPatch, boolean post) {
|
||||
_uploadBuffer = null;
|
||||
_uploadBufferLength = 0;
|
||||
if (uploading) {
|
||||
if (uploadBuffer == null) {
|
||||
uploadBuffer = new byte[0];
|
||||
}
|
||||
_uploadBuffer = new ByteArrayInputStream(uploadBuffer);
|
||||
_uploadBufferLength = uploadBuffer.length;
|
||||
_method = "PUT";
|
||||
return;
|
||||
}
|
||||
|
||||
if (usingPatch) {
|
||||
_method = "PATCH";
|
||||
return;
|
||||
}
|
||||
|
||||
if (uploadBuffer != null && uploadBuffer.length > 0) {
|
||||
_uploadBuffer = new ByteArrayInputStream(uploadBuffer);
|
||||
_uploadBufferLength = uploadBuffer.length;
|
||||
post = true;
|
||||
}
|
||||
|
||||
if (post) {
|
||||
_method = "POST";
|
||||
_requestHeaders.put("content-type", "application/x-www-form-urlencoded");
|
||||
}
|
||||
}
|
||||
|
||||
private void setupMultipartForm(String[] formFields, String[] formFiles) {
|
||||
if ((formFields.length & 1) != 0) {
|
||||
throw new IllegalArgumentException("formFields has odd length");
|
||||
}
|
||||
if ((formFiles.length & 1) != 0) {
|
||||
throw new IllegalArgumentException("formFiles has odd length");
|
||||
}
|
||||
|
||||
SecureRandom rnd = new SecureRandom();
|
||||
String boundary = "ScummVM-Boundary-" + (new BigInteger(128, rnd)).toString(10);
|
||||
|
||||
int contentLength = 0;
|
||||
Vector<InputStream> bodyParts = new Vector<>(formFiles.length * 2 + 2);
|
||||
|
||||
_method = "POST";
|
||||
_requestHeaders.put("content-type", String.format("multipart/form-data; boundary=%s", boundary));
|
||||
|
||||
StringBuilder formFieldsContent = new StringBuilder();
|
||||
for (int i = 0; i < formFields.length; i += 2) {
|
||||
formFieldsContent.append(String.format("\r\n--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n", boundary, formFields[i]));
|
||||
formFieldsContent.append(formFields[i+1]);
|
||||
}
|
||||
for (int i = 0; i < formFiles.length; i += 2) {
|
||||
File file = new File(formFiles[i+1]);
|
||||
formFieldsContent.append(String.format("\r\n--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n\r\n", boundary, formFiles[i], file.getName()));
|
||||
|
||||
byte[] textContent = formFieldsContent.toString().getBytes(Charset.defaultCharset());
|
||||
bodyParts.add(new ByteArrayInputStream(textContent));
|
||||
if (contentLength >= 0) {
|
||||
try {
|
||||
contentLength = Math.addExact(contentLength, textContent.length);
|
||||
} catch (ArithmeticException e) {
|
||||
contentLength = -1;
|
||||
}
|
||||
}
|
||||
formFieldsContent = new StringBuilder();
|
||||
|
||||
try {
|
||||
SAFFSTree.PathResult pr = SAFFSTree.fullPathToNode(null, file.getPath(), false);
|
||||
if (pr == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
bodyParts.add(new FileInputStream(file));
|
||||
long length = file.length();
|
||||
if (!file.isFile() || length >= Integer.MAX_VALUE) {
|
||||
contentLength = -1;
|
||||
} else if (contentLength >= 0) {
|
||||
try {
|
||||
contentLength = Math.addExact(contentLength, (int)length);
|
||||
} catch (ArithmeticException e) {
|
||||
contentLength = -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ParcelFileDescriptor pfd = pr.tree.createFileDescriptor(pr.node, "r");
|
||||
bodyParts.add(new ParcelFileDescriptor.AutoCloseInputStream(pfd));
|
||||
contentLength = -1;
|
||||
}
|
||||
} catch (FileNotFoundException ignored) {
|
||||
// We can't trigger an error now: we will make sure we call finished later with an error
|
||||
bodyParts.add(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Now we only have to close the multipart with an ending boundary
|
||||
formFieldsContent.append(String.format("\r\n--%s--\r\n", boundary));
|
||||
byte[] textContent = formFieldsContent.toString().getBytes(Charset.defaultCharset());
|
||||
bodyParts.add(new ByteArrayInputStream(textContent));
|
||||
if (contentLength >= 0) {
|
||||
try {
|
||||
contentLength = Math.addExact(contentLength, textContent.length);
|
||||
} catch (ArithmeticException e) {
|
||||
contentLength = -1;
|
||||
}
|
||||
}
|
||||
_uploadBuffer = new SequenceInputStream(bodyParts.elements());
|
||||
_uploadBufferLength = contentLength;
|
||||
}
|
||||
|
||||
private void setupHeaders(String[] requestHeaders) {
|
||||
if ((requestHeaders.length & 1) != 0) {
|
||||
throw new IllegalArgumentException("requestHeaders has odd length");
|
||||
}
|
||||
for(int i = 0; i < requestHeaders.length; i += 2) {
|
||||
if (requestHeaders[i] == null) {
|
||||
// If there were invalid headers passed in native code
|
||||
// we end up with null entries at the end of the array
|
||||
return;
|
||||
}
|
||||
_requestHeaders.put(requestHeaders[i], requestHeaders[i+1]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* Called from JNI
|
||||
*/
|
||||
public void cancel() {
|
||||
_cancelled.set(true);
|
||||
// Don't notify the native object if we got cancelled: it may have been reused
|
||||
_nativePointer = 0;
|
||||
}
|
||||
|
||||
public String getURL() {
|
||||
return _url.get();
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
if (_uploadBuffer != null) {
|
||||
try {
|
||||
_uploadBuffer.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Runs on HTTPManager thread pool
|
||||
@Override
|
||||
public void run() {
|
||||
if (_cancelled.get()) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
URL url;
|
||||
HttpURLConnection urlConnection;
|
||||
try {
|
||||
url = new URL(_url.get());
|
||||
Log.d(LOG_TAG, String.format("Will make HTTP request to %s with method %s", url, _method));
|
||||
urlConnection = (HttpURLConnection) url.openConnection();
|
||||
urlConnection.setRequestMethod(_method);
|
||||
} catch (IOException e) {
|
||||
final String errorMsg = e.getMessage();
|
||||
_manager.enqueue(() -> finished(_nativePointer, -1, errorMsg));
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cancelled.get()) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
urlConnection.setInstanceFollowRedirects(true);
|
||||
for (Map.Entry<String, String> e : _requestHeaders.entrySet()) {
|
||||
urlConnection.addRequestProperty(e.getKey(), e.getValue());
|
||||
}
|
||||
if (_uploadBuffer != null) {
|
||||
urlConnection.setDoOutput(true);
|
||||
if (_uploadBufferLength != -1) {
|
||||
urlConnection.setFixedLengthStreamingMode(_uploadBufferLength);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
urlConnection.connect();
|
||||
if (_cancelled.get()) {
|
||||
urlConnection.disconnect();
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_uploadBuffer != null) {
|
||||
try (OutputStream out = urlConnection.getOutputStream()) {
|
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
int read;
|
||||
while ((read = _uploadBuffer.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
// We failed to open some file when building the buffer
|
||||
_manager.enqueue(() -> finished(_nativePointer, -1, "Can't open file"));
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_cancelled.get()) {
|
||||
urlConnection.disconnect();
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, java.util.List<String>> headersField = urlConnection.getHeaderFields();
|
||||
if (_cancelled.get()) {
|
||||
urlConnection.disconnect();
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
final Vector<String> headers = new Vector<>(headersField.size() * 2);
|
||||
for (Map.Entry<String, java.util.List<String>> e : headersField.entrySet()) {
|
||||
String key = e.getKey();
|
||||
if (key == null) {
|
||||
// The status line is placed in the map with a null key: ignore it
|
||||
continue;
|
||||
}
|
||||
List<String> values = e.getValue();
|
||||
headers.add(key.toLowerCase(Locale.ROOT));
|
||||
headers.add(values.get(values.size() - 1));
|
||||
}
|
||||
_manager.enqueue(() -> gotHeaders(_nativePointer, headers.toArray(new String[0])));
|
||||
|
||||
int contentLength = urlConnection.getContentLength();
|
||||
|
||||
InputStream in = urlConnection.getInputStream();
|
||||
if (_cancelled.get()) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean finished = false;
|
||||
while(!finished) {
|
||||
final byte[] inputData = new byte[DEFAULT_BUFFER_SIZE];
|
||||
int offset = 0;
|
||||
while(offset < DEFAULT_BUFFER_SIZE) {
|
||||
final int size = in.read(inputData, offset, DEFAULT_BUFFER_SIZE - offset);
|
||||
if (size == -1) {
|
||||
finished = true;
|
||||
break;
|
||||
}
|
||||
if (_cancelled.get()) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
offset += size;
|
||||
}
|
||||
|
||||
final int offset_ = offset;
|
||||
_manager.enqueue(() -> gotData(_nativePointer, inputData, offset_, contentLength));
|
||||
}
|
||||
// Update URL field
|
||||
url = urlConnection.getURL();
|
||||
_url.set(url.toExternalForm());
|
||||
|
||||
final int responseCode = urlConnection.getResponseCode();
|
||||
_manager.enqueue(() -> finished(_nativePointer, responseCode, null));
|
||||
} catch (FileNotFoundException e) {
|
||||
// The server returned an error, return the error code and no data
|
||||
int responseCode = -1;
|
||||
try {
|
||||
responseCode = urlConnection.getResponseCode();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
final int responseCode_ = responseCode;
|
||||
final String errorMsg = e.getMessage();
|
||||
_manager.enqueue(() -> finished(_nativePointer, responseCode_, errorMsg));
|
||||
cleanup();
|
||||
} catch (IOException e) {
|
||||
Log.w(LOG_TAG, "Error when making HTTP request", e);
|
||||
final String errorMsg = e.getMessage();
|
||||
_manager.enqueue(() -> finished(_nativePointer, -1, errorMsg));
|
||||
cleanup();
|
||||
} finally {
|
||||
urlConnection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package org.scummvm.scummvm.net;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
/*
|
||||
* Inspiration taken from http://blog.novoj.net/2016/02/29/how-to-make-apache-httpclient-trust-lets-encrypt-certificate-authority/
|
||||
*/
|
||||
|
||||
class LETrustManager implements X509TrustManager {
|
||||
private static final String[] derLECerts = {
|
||||
/* ISRG Root X1 */ "MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZLubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=",
|
||||
/* ISRG Root X2 */ "MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBTZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZIzj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdWtL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1/q4AaOeMSQ+2b1tbFfLn",
|
||||
};
|
||||
|
||||
private static LETrustManager instance;
|
||||
|
||||
private final X509TrustManager _systemTrustManager;
|
||||
private final X509TrustManager _leTrustManager;
|
||||
|
||||
static SSLContext getContext() throws NoSuchAlgorithmException, KeyManagementException {
|
||||
try {
|
||||
if (instance == null) {
|
||||
instance = new LETrustManager();
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace(System.err);
|
||||
return SSLContext.getDefault();
|
||||
} catch (KeyStoreException e) {
|
||||
e.printStackTrace(System.err);
|
||||
return SSLContext.getDefault();
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace(System.err);
|
||||
return SSLContext.getDefault();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace(System.err);
|
||||
return SSLContext.getDefault();
|
||||
}
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, new TrustManager[]{instance}, null);
|
||||
return sslContext;
|
||||
}
|
||||
|
||||
public LETrustManager() throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException {
|
||||
final TrustManagerFactory mainTrustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
mainTrustFactory.init((KeyStore)null);
|
||||
this._systemTrustManager = (X509TrustManager)mainTrustFactory.getTrustManagers()[0];
|
||||
|
||||
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
ks.load(null);
|
||||
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
int i = 1;
|
||||
for (String derCert : derLECerts) {
|
||||
ByteArrayInputStream is = new ByteArrayInputStream(Base64.decode(derCert, Base64.DEFAULT));
|
||||
Certificate cert = cf.generateCertificate(is);
|
||||
ks.setCertificateEntry(String.format(Locale.getDefault(), "%d", i), cert);
|
||||
i++;
|
||||
}
|
||||
|
||||
final TrustManagerFactory leTrustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
leTrustFactory.init(ks);
|
||||
this._leTrustManager = (X509TrustManager)leTrustFactory.getTrustManagers()[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(final X509Certificate[] x509Certificates, final String authType) throws CertificateException {
|
||||
// LE doesn't issue client certificates
|
||||
_systemTrustManager.checkClientTrusted(x509Certificates, authType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(final X509Certificate[] x509Certificates, final String authType) throws CertificateException {
|
||||
try {
|
||||
_systemTrustManager.checkServerTrusted(x509Certificates, authType);
|
||||
} catch(CertificateException ignored) {
|
||||
this._leTrustManager.checkServerTrusted(x509Certificates, authType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
X509Certificate[] systemAccepted = this._systemTrustManager.getAcceptedIssuers();
|
||||
X509Certificate[] leAccepted = this._leTrustManager.getAcceptedIssuers();
|
||||
|
||||
X509Certificate[] allAccepted = Arrays.copyOf(systemAccepted, systemAccepted.length + leAccepted.length);
|
||||
System.arraycopy(leAccepted, 0, allAccepted, systemAccepted.length, leAccepted.length);
|
||||
|
||||
return allAccepted;
|
||||
}
|
||||
}
|
||||
267
backends/platform/android/org/scummvm/scummvm/net/SSocket.java
Normal file
267
backends/platform/android/org/scummvm/scummvm/net/SSocket.java
Normal file
@@ -0,0 +1,267 @@
|
||||
package org.scummvm.scummvm.net;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
/** @noinspection unused*/
|
||||
public class SSocket {
|
||||
final static String LOG_TAG = "ScummVM";
|
||||
|
||||
protected Socket _socket;
|
||||
|
||||
protected int _buffer = -2;
|
||||
|
||||
public SSocket(String url_) {
|
||||
final URL url;
|
||||
try {
|
||||
url = new URL(url_);
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
String scheme = url.getProtocol().toLowerCase();
|
||||
if (!scheme.equals("http") && !scheme.equals("https")) {
|
||||
throw new RuntimeException("Unsupported protocol");
|
||||
}
|
||||
|
||||
String host = url.getHost();
|
||||
if (host == null) {
|
||||
throw new RuntimeException("Missing host name");
|
||||
}
|
||||
if (host.contains(":")) {
|
||||
host = String.format("[%s]", host);
|
||||
}
|
||||
int port = url.getPort();
|
||||
if (port == -1) {
|
||||
port = url.getDefaultPort();
|
||||
}
|
||||
|
||||
Socket socket;
|
||||
try {
|
||||
socket = proxyConnect(url, host, port);
|
||||
|
||||
if (scheme.equals("https")) {
|
||||
SSLSocketFactory ssf = new TLSSocketFactory();
|
||||
socket = ssf.createSocket(socket, host, port, true);
|
||||
}
|
||||
|
||||
_socket = socket;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Socket proxyConnect(URL url, String host, int port) throws IOException {
|
||||
Socket ret;
|
||||
|
||||
final URI uri;
|
||||
try {
|
||||
uri = url.toURI();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
SocketAddress sa = new InetSocketAddress(host, port);
|
||||
|
||||
final ProxySelector proxySelector = ProxySelector.getDefault();
|
||||
final List<Proxy> proxies = proxySelector.select(uri);
|
||||
|
||||
IOException lastExc = null;
|
||||
for (Proxy proxy : proxies) {
|
||||
final Proxy.Type proxyType = proxy.type();
|
||||
try {
|
||||
if (proxyType != Proxy.Type.HTTP) {
|
||||
ret = new Socket(proxy);
|
||||
ret.connect(sa);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// HTTP proxy with Socket is not supported on Android
|
||||
// Let's do it ourselves with a CONNECT method
|
||||
|
||||
// First, resolve the proxy address: it's not resolved in Proxy
|
||||
InetSocketAddress proxyAddress = (InetSocketAddress)proxy.address();
|
||||
InetAddress addr = proxyAddress.getAddress();
|
||||
String proxyHost;
|
||||
if (addr != null) {
|
||||
proxyHost = addr.getHostName();
|
||||
} else {
|
||||
proxyHost = proxyAddress.getHostName();
|
||||
}
|
||||
int proxyPort = proxyAddress.getPort();
|
||||
proxyAddress = new InetSocketAddress(proxyHost, proxyPort);
|
||||
|
||||
ret = new Socket();
|
||||
ret.connect(proxyAddress);
|
||||
|
||||
proxyHTTPConnect(ret, host, port);
|
||||
return ret;
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Got an exception while connecting", e);
|
||||
if (proxy.address() != null) {
|
||||
proxySelector.connectFailed(uri, proxy.address(), e);
|
||||
}
|
||||
lastExc = e;
|
||||
}
|
||||
}
|
||||
if (lastExc == null) {
|
||||
throw new RuntimeException("No proxy specified");
|
||||
}
|
||||
throw lastExc;
|
||||
}
|
||||
|
||||
private static void proxyHTTPConnect(Socket socket, String host, int port) throws IOException {
|
||||
String requestLine = String.format(Locale.ROOT, "CONNECT %s:%d HTTP/1.0\r\n\r\n", host, port);
|
||||
socket.getOutputStream().write(requestLine.getBytes());
|
||||
byte[] buffer = readLine(socket);
|
||||
|
||||
// HTTP/1.x SP 2xx SP
|
||||
if (buffer.length < 13 ||
|
||||
buffer[0] != 'H' ||
|
||||
buffer[1] != 'T' ||
|
||||
buffer[2] != 'T' ||
|
||||
buffer[3] != 'P' ||
|
||||
buffer[4] != '/' ||
|
||||
buffer[5] != '1' ||
|
||||
buffer[6] != '.' ||
|
||||
(buffer[7] != '0' && buffer[7] != '1') ||
|
||||
buffer[8] != ' ' ||
|
||||
buffer[9] != '2' ||
|
||||
!Character.isDigit(buffer[10]) ||
|
||||
!Character.isDigit(buffer[11]) ||
|
||||
buffer[12] != ' ') {
|
||||
throw new IOException("Invalid proxy reply");
|
||||
}
|
||||
|
||||
for (int i = 0; i < 64 && buffer.length > 0; i++) {
|
||||
buffer = readLine(socket);
|
||||
}
|
||||
if (buffer.length > 0) {
|
||||
throw new IOException("Invalid proxy reply: too much headers");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readLine(Socket socket) throws IOException {
|
||||
InputStream is = socket.getInputStream();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
int b;
|
||||
while (true) {
|
||||
b = is.read();
|
||||
if (b == -1) {
|
||||
return baos.toByteArray();
|
||||
}
|
||||
if (b == '\r') {
|
||||
continue;
|
||||
}
|
||||
if (b == '\n') {
|
||||
return baos.toByteArray();
|
||||
}
|
||||
baos.write(b);
|
||||
}
|
||||
}
|
||||
|
||||
public int ready() {
|
||||
if (_buffer != -2) {
|
||||
// We have at least one byte or an EOF
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
// Set receive timeout to something ridiculously low to mimic a non-blocking socket
|
||||
_socket.setSoTimeout(1);
|
||||
_buffer = _socket.getInputStream().read();
|
||||
return 1;
|
||||
} catch (SocketTimeoutException e) {
|
||||
// Nothing was ready to consume
|
||||
return 0;
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Got an exception while checking ready status", e);
|
||||
// Make it like if there was something ready
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public int send(byte[] data) {
|
||||
try {
|
||||
// Setup unlimited read timeout to allow for SSL exchanges to work
|
||||
_socket.setSoTimeout(0);
|
||||
_socket.getOutputStream().write(data);
|
||||
return data.length;
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Got an exception while sending socket data", e);
|
||||
// This likely failed
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int recv(byte[] data) {
|
||||
if (data.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (_buffer == -1) {
|
||||
_buffer = -2;
|
||||
return -1;
|
||||
}
|
||||
int offset = 0;
|
||||
if (_buffer != -2) {
|
||||
data[0] = (byte)_buffer;
|
||||
offset = 1;
|
||||
_buffer = -2;
|
||||
}
|
||||
try {
|
||||
int recvd = 0;
|
||||
long end = System.currentTimeMillis() + 5000;
|
||||
while (true) {
|
||||
try {
|
||||
// Allow for some timeout but not too much
|
||||
_socket.setSoTimeout(500);
|
||||
recvd = _socket.getInputStream().read(data, offset, data.length - offset);
|
||||
break;
|
||||
} catch (SocketTimeoutException e1) {
|
||||
if (System.currentTimeMillis() >= end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (offset == 0) {
|
||||
// Nothing was buffered
|
||||
return recvd;
|
||||
}
|
||||
if (recvd == -1) {
|
||||
// Buffer the EOF and return the previous buffered data;
|
||||
_buffer = -1;
|
||||
return offset;
|
||||
}
|
||||
return offset + recvd;
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Got an exception while receiving socket data", e);
|
||||
return offset;
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
_socket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Got an exception while closing socket", e);
|
||||
}
|
||||
_socket = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package org.scummvm.scummvm.net;
|
||||
|
||||
/*
|
||||
* Customized from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/
|
||||
* Added TLS1.3 support and keep old protocols enabled for maximum compatibility
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
/**
|
||||
* @author fkrauthan
|
||||
*/
|
||||
public class TLSSocketFactory extends SSLSocketFactory {
|
||||
|
||||
private final SSLSocketFactory _factory;
|
||||
private static String[] _protocols;
|
||||
|
||||
private static boolean _init;
|
||||
|
||||
public static void init() {
|
||||
if (_init) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(new TLSSocketFactory());
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
_init = true;
|
||||
}
|
||||
|
||||
public TLSSocketFactory() {
|
||||
SSLContext context = null;
|
||||
try {
|
||||
context = LETrustManager.getContext();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (KeyManagementException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
_factory = context.getSocketFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getDefaultCipherSuites() {
|
||||
return _factory.getDefaultCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedCipherSuites() {
|
||||
return _factory.getSupportedCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket() throws IOException {
|
||||
return enableTLSOnSocket(_factory.createSocket());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
||||
return enableTLSOnSocket(_factory.createSocket(s, host, port, autoClose));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
|
||||
return enableTLSOnSocket(_factory.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
|
||||
return enableTLSOnSocket(_factory.createSocket(host, port, localHost, localPort));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||
return enableTLSOnSocket(_factory.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||
return enableTLSOnSocket(_factory.createSocket(address, port, localAddress, localPort));
|
||||
}
|
||||
|
||||
private Socket enableTLSOnSocket(Socket socket) {
|
||||
if(socket instanceof SSLSocket) {
|
||||
SSLSocket sslSocket = (SSLSocket)socket;
|
||||
|
||||
if (_protocols == null) {
|
||||
String[] newProtocols = {"TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"};
|
||||
|
||||
// Build the list of protocols to enable
|
||||
Set<String> protocols = new HashSet<>(Arrays.asList(sslSocket.getEnabledProtocols()));
|
||||
Set<String> supported = new HashSet<>(Arrays.asList(sslSocket.getSupportedProtocols()));
|
||||
for (String protocol : newProtocols) {
|
||||
if (protocols.contains(protocol)) {
|
||||
continue;
|
||||
}
|
||||
if (!supported.contains(protocol)) {
|
||||
continue;
|
||||
}
|
||||
protocols.add(protocol);
|
||||
}
|
||||
_protocols = protocols.toArray(new String[]{});
|
||||
}
|
||||
|
||||
sslSocket.setEnabledProtocols(_protocols);
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
261
backends/platform/android/org/scummvm/scummvm/zip/ZipCoder.java
Normal file
261
backends/platform/android/org/scummvm/scummvm/zip/ZipCoder.java
Normal file
@@ -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] == '/';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
799
backends/platform/android/org/scummvm/scummvm/zip/ZipEntry.java
Normal file
799
backends/platform/android/org/scummvm/scummvm/zip/ZipEntry.java
Normal file
@@ -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 "accidentally" 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2130
backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java
Normal file
2130
backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java
Normal file
File diff suppressed because it is too large
Load Diff
356
backends/platform/android/org/scummvm/scummvm/zip/ZipUtils.java
Normal file
356
backends/platform/android/org/scummvm/scummvm/zip/ZipUtils.java
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user