(Android) Updated the game launching logic

This commit is contained in:
Leon Styhre 2023-12-07 18:51:09 +01:00
parent d87bb4e3a4
commit 3ae7bc6b5b
4 changed files with 230 additions and 115 deletions

2
.gitignore vendored
View file

@ -34,7 +34,7 @@ emulationstation.worker.js
/ROMs /ROMs
# Android build # Android build
android /android
# AppImage # AppImage
AppDir AppDir

View file

@ -882,6 +882,7 @@ void FileData::launchGame()
std::string commandRaw {command}; std::string commandRaw {command};
std::string romPath {Utils::FileSystem::getEscapedPath(mPath)}; std::string romPath {Utils::FileSystem::getEscapedPath(mPath)};
std::string baseName {Utils::FileSystem::getStem(mPath)}; std::string baseName {Utils::FileSystem::getStem(mPath)};
std::string romRaw {Utils::FileSystem::getPreferredPath(mPath)};
// For the special case where a directory has a supported file extension and is therefore // For the special case where a directory has a supported file extension and is therefore
// interpreted as a file, check if there is a matching filename inside the directory. // interpreted as a file, check if there is a matching filename inside the directory.
@ -890,7 +891,11 @@ void FileData::launchGame()
for (std::string& file : Utils::FileSystem::getDirContent(mPath)) { for (std::string& file : Utils::FileSystem::getDirContent(mPath)) {
if (Utils::FileSystem::getFileName(file) == Utils::FileSystem::getFileName(mPath) && if (Utils::FileSystem::getFileName(file) == Utils::FileSystem::getFileName(mPath) &&
(Utils::FileSystem::isRegularFile(file) || Utils::FileSystem::isSymlink(file))) { (Utils::FileSystem::isRegularFile(file) || Utils::FileSystem::isSymlink(file))) {
#if defined(__ANDROID__)
romRaw = file;
#else
romPath = Utils::FileSystem::getEscapedPath(file); romPath = Utils::FileSystem::getEscapedPath(file);
#endif
baseName = baseName.substr(0, baseName.find(".")); baseName = baseName.substr(0, baseName.find("."));
break; break;
} }
@ -898,7 +903,6 @@ void FileData::launchGame()
} }
const std::string fileName {baseName + Utils::FileSystem::getExtension(romPath)}; const std::string fileName {baseName + Utils::FileSystem::getExtension(romPath)};
const std::string romRaw {Utils::FileSystem::getPreferredPath(mPath)};
const std::string esPath {Utils::FileSystem::getExePath()}; const std::string esPath {Utils::FileSystem::getExePath()};
bool runInBackground {false}; bool runInBackground {false};
@ -951,8 +955,12 @@ void FileData::launchGame()
std::string androidPackage; std::string androidPackage;
std::string androidActivity; std::string androidActivity;
std::string androidAction; std::string androidAction;
std::string androidFileAsURI; std::string androidCategory;
std::vector<std::pair<std::string, std::string>> androidExtras; std::string androidMimeType;
std::string androidData;
std::map<std::string, std::string> androidExtrasString;
std::map<std::string, std::string> androidExtrasBool;
std::vector<std::string> androidActivityFlags;
#endif #endif
#if defined(_WIN64) #if defined(_WIN64)
@ -1119,18 +1127,6 @@ void FileData::launchGame()
androidPackage = androidPackage.substr(0, separatorPos); androidPackage = androidPackage.substr(0, separatorPos);
} }
separatorPos = androidPackage.find('|');
if (separatorPos != std::string::npos) {
androidAction = androidPackage.substr(separatorPos + 1);
androidPackage = androidPackage.substr(0, separatorPos);
}
separatorPos = androidActivity.find('|');
if (separatorPos != std::string::npos) {
androidAction = androidActivity.substr(separatorPos + 1);
androidActivity = androidActivity.substr(0, separatorPos);
}
LOG(LogDebug) << "FileData::launchGame(): Found emulator package \"" << androidPackage LOG(LogDebug) << "FileData::launchGame(): Found emulator package \"" << androidPackage
<< "\""; << "\"";
} }
@ -1609,6 +1605,7 @@ void FileData::launchGame()
#endif #endif
#endif #endif
#if !defined(__ANDROID__)
// Replace the remaining variables with their actual values. // Replace the remaining variables with their actual values.
command = Utils::String::replace(command, "%ROM%", romPath); command = Utils::String::replace(command, "%ROM%", romPath);
command = Utils::String::replace(command, "%BASENAME%", baseName); command = Utils::String::replace(command, "%BASENAME%", baseName);
@ -1616,12 +1613,64 @@ void FileData::launchGame()
command = Utils::String::replace(command, "%ROMRAW%", romRaw); command = Utils::String::replace(command, "%ROMRAW%", romRaw);
command = Utils::String::replace(command, "%ROMPATH%", command = Utils::String::replace(command, "%ROMPATH%",
Utils::FileSystem::getEscapedPath(getROMDirectory())); Utils::FileSystem::getEscapedPath(getROMDirectory()));
#if defined(__ANDROID__) #else
if (command.find("%ROMURI%") != std::string::npos)
androidFileAsURI = romRaw;
command = Utils::String::replace(command, "%ANDROIDPACKAGE%", androidPackage); command = Utils::String::replace(command, "%ANDROIDPACKAGE%", androidPackage);
size_t extraPos {command.find("%EXTRA_")};
const std::vector<std::string> androidVariabels {
"%ACTION%=", "%CATEGORY%=", "%MIMETYPE%=", "%DATA%="};
for (std::string variable : androidVariabels) {
size_t dataPos {command.find(variable)};
if (dataPos != std::string::npos) {
bool invalidEntry {false};
bool isQuoted {(command.length() > dataPos + variable.length() &&
command[dataPos + variable.length()] == '\"')};
std::string value;
if (isQuoted) {
const size_t closeQuotePos {command.find("\"", dataPos + variable.length() + 1)};
if (closeQuotePos != std::string::npos)
value = command.substr(dataPos + variable.length() + 1,
closeQuotePos - (dataPos + variable.length() + 1));
else
invalidEntry = true;
}
else {
const size_t spacePos {command.find(" ", dataPos)};
if (spacePos != std::string::npos)
value = command.substr(dataPos + variable.length(),
spacePos - (dataPos + variable.length()));
else
value = command.substr(dataPos + variable.length(),
command.size() - dataPos + variable.length());
}
if (invalidEntry) {
LOG(LogError) << "Invalid entry in systems configuration file es_systems.xml";
LOG(LogError) << "Raw emulator launch command:";
LOG(LogError) << commandRaw;
window->queueInfoPopup("ERROR: INVALID ENTRY IN SYSTEMS CONFIGURATION FILE", 6000);
window->setAllowTextScrolling(true);
window->setAllowFileAnimation(true);
return;
}
if (variable == "%ACTION%=")
androidAction = value;
else if (variable == "%DATA%=")
androidData = value;
else if (variable == "%CATEGORY%=")
androidCategory = value;
else if (variable == "%MIMETYPE%=")
androidMimeType = value;
}
}
std::vector<std::string> extraVariabels {"%EXTRA_", "%EXTRABOOL_"};
for (std::string variable : extraVariabels) {
size_t extraPos {command.find(variable)};
while (extraPos != std::string::npos) { while (extraPos != std::string::npos) {
if (extraPos != std::string::npos) { if (extraPos != std::string::npos) {
bool invalidEntry {false}; bool invalidEntry {false};
@ -1633,19 +1682,21 @@ void FileData::launchGame()
if (equalPos == std::string::npos) if (equalPos == std::string::npos)
invalidEntry = true; invalidEntry = true;
if (!invalidEntry && extraPos + 8 >= command.size()) if (!invalidEntry && extraPos + variable.length() + 1 >= command.size())
invalidEntry = true; invalidEntry = true;
if (!invalidEntry) { if (!invalidEntry) {
if (command.length() > equalPos && command[equalPos + 1] == '\"') if (command.length() > equalPos && command[equalPos + 1] == '\"')
isQuoted = true; isQuoted = true;
extraName = command.substr(extraPos + 7, equalPos - (extraPos + 8)); extraName = command.substr(extraPos + variable.length(),
equalPos - (extraPos + variable.length() + 1));
if (isQuoted) { if (isQuoted) {
const size_t closeQuotePos {command.find("\"", equalPos + 2)}; const size_t closeQuotePos {command.find("\"", equalPos + 2)};
if (closeQuotePos != std::string::npos) if (closeQuotePos != std::string::npos)
extraValue = command.substr(equalPos + 2, closeQuotePos - (equalPos + 2)); extraValue =
command.substr(equalPos + 2, closeQuotePos - (equalPos + 2));
else else
invalidEntry = true; invalidEntry = true;
} }
@ -1658,7 +1709,8 @@ void FileData::launchGame()
} }
if (invalidEntry) { if (invalidEntry) {
LOG(LogError) << "Invalid entry in systems configuration file es_systems.xml"; LOG(LogError)
<< "Invalid entry in systems configuration file es_systems.xml";
LOG(LogError) << "Raw emulator launch command:"; LOG(LogError) << "Raw emulator launch command:";
LOG(LogError) << commandRaw; LOG(LogError) << commandRaw;
@ -1669,12 +1721,24 @@ void FileData::launchGame()
return; return;
} }
if (extraName != "" && extraValue != "") if (extraName != "" && extraValue != "") {
androidExtras.emplace_back(make_pair(extraName, extraValue)); if (variable == "%EXTRA_")
androidExtrasString[extraName] = extraValue;
else if (variable == "%EXTRABOOL_")
androidExtrasBool[extraName] = extraValue;
} }
} }
extraPos = command.find("%EXTRA_", extraPos + 1);
} }
extraPos = command.find(variable, extraPos + 1);
}
}
if (command.find("%ACTIVITY_CLEAR_TASK%") != std::string::npos)
androidActivityFlags.emplace_back("%ACTIVITY_CLEAR_TASK%");
if (command.find("%ACTIVITY_CLEAR_TOP%") != std::string::npos)
androidActivityFlags.emplace_back("%ACTIVITY_CLEAR_TOP%");
if (command.find("%ACTIVITY_NO_HISTORY%") != std::string::npos)
androidActivityFlags.emplace_back("%ACTIVITY_NO_HISTORY%");
#endif #endif
#if defined(_WIN64) #if defined(_WIN64)
@ -1722,15 +1786,29 @@ void FileData::launchGame()
#if defined(__ANDROID__) #if defined(__ANDROID__)
LOG(LogInfo) << "Expanded emulator launch arguments:"; LOG(LogInfo) << "Expanded emulator launch arguments:";
LOG(LogInfo) << "Package: " << androidPackage; LOG(LogInfo) << "Package: " << androidPackage;
LOG(LogInfo) << "Activity: " << (androidActivity == "" ? "<package default>" : androidActivity); if (androidActivity != "") {
LOG(LogInfo) << "Action: " << (androidAction == "" ? "<package default>" : androidAction); LOG(LogInfo) << "Activity: " << androidActivity;
if (androidFileAsURI != "") {
LOG(LogInfo) << "File (URI): " << androidFileAsURI;
} }
for (auto& extra : androidExtras) { if (androidAction != "") {
LOG(LogInfo) << "Action: " << androidAction;
}
if (androidCategory != "") {
LOG(LogInfo) << "Category: " << androidCategory;
}
if (androidMimeType != "") {
LOG(LogInfo) << "MIME type: " << androidMimeType;
}
if (androidData != "") {
LOG(LogInfo) << "Data: " << androidData;
}
for (auto& extra : androidExtrasString) {
LOG(LogInfo) << "Extra name: " << extra.first; LOG(LogInfo) << "Extra name: " << extra.first;
LOG(LogInfo) << "Extra value: " << extra.second; LOG(LogInfo) << "Extra value: " << extra.second;
} }
for (auto& extra : androidExtrasBool) {
LOG(LogInfo) << "Extra bool name: " << extra.first;
LOG(LogInfo) << "Extra bool value: " << extra.second;
}
#else #else
LOG(LogInfo) << "Expanded emulator launch command:"; LOG(LogInfo) << "Expanded emulator launch command:";
LOG(LogInfo) << command; LOG(LogInfo) << command;
@ -1755,13 +1833,14 @@ void FileData::launchGame()
Utils::String::stringToWideString(startDirectory), runInBackground, hideWindow); Utils::String::stringToWideString(startDirectory), runInBackground, hideWindow);
#elif defined(__ANDROID__) #elif defined(__ANDROID__)
returnValue = Utils::Platform::Android::launchGame( returnValue = Utils::Platform::Android::launchGame(
androidPackage, androidActivity, androidAction, androidFileAsURI, androidExtras); androidPackage, androidActivity, androidAction, androidCategory, androidMimeType,
androidData, romRaw, androidExtrasString, androidExtrasBool, androidActivityFlags);
#else #else
returnValue = Utils::Platform::launchGameUnix(command, startDirectory, runInBackground); returnValue = Utils::Platform::launchGameUnix(command, startDirectory, runInBackground);
#endif #endif
// Notify the user in case of a failed game launch using a popup window. // Notify the user in case of a failed game launch using a popup window.
if (returnValue != 0) { if (returnValue != 0) {
LOG(LogWarning) << "...launch terminated with nonzero return value " << returnValue; LOG(LogWarning) << "Launch terminated with nonzero return value " << returnValue;
window->queueInfoPopup("ERROR LAUNCHING GAME '" + window->queueInfoPopup("ERROR LAUNCHING GAME '" +
Utils::String::toUpper(metadata.get("name")) + "' (ERROR CODE " + Utils::String::toUpper(metadata.get("name")) + "' (ERROR CODE " +
@ -2051,13 +2130,10 @@ const std::pair<std::string, FileData::findEmulatorResult> FileData::findEmulato
for (std::string& androidpackage : emulatorAndroidPackages) { for (std::string& androidpackage : emulatorAndroidPackages) {
// If a forward slash character is present in the androidpackage entry it means an explicit // If a forward slash character is present in the androidpackage entry it means an explicit
// Intent activity should be used rather than the default one. The checkEmulatorInstalled() // Intent activity should be used rather than the default one. The checkEmulatorInstalled()
// Java function will check for the activity as well and if it's not found it flags // Java function will check for the activity as well and if it's not found it flags the
// the overall emulator entry as not found. It's also possible to define an explicit // overall emulator entry as not found.
// Intent action using the pipe character but this is not checked for in the Java
// function as invalid actions will not lead to crashes.
std::string packageName {androidpackage}; std::string packageName {androidpackage};
std::string activity; std::string activity;
std::string action;
size_t separatorPos {packageName.find('/')}; size_t separatorPos {packageName.find('/')};
if (separatorPos != std::string::npos) { if (separatorPos != std::string::npos) {
@ -2065,18 +2141,6 @@ const std::pair<std::string, FileData::findEmulatorResult> FileData::findEmulato
packageName = packageName.substr(0, separatorPos); packageName = packageName.substr(0, separatorPos);
} }
separatorPos = packageName.find('|');
if (separatorPos != std::string::npos) {
action = packageName.substr(separatorPos + 1);
packageName = packageName.substr(0, separatorPos);
}
separatorPos = activity.find('|');
if (separatorPos != std::string::npos) {
action = activity.substr(separatorPos + 1);
activity = activity.substr(0, separatorPos);
}
if (Utils::Platform::Android::checkEmulatorInstalled(packageName, activity)) { if (Utils::Platform::Android::checkEmulatorInstalled(packageName, activity)) {
return std::make_pair(androidpackage, return std::make_pair(androidpackage,
FileData::findEmulatorResult::FOUND_ANDROID_PACKAGE); FileData::findEmulatorResult::FOUND_ANDROID_PACKAGE);

View file

@ -31,6 +31,18 @@
#include <array> #include <array>
#include <fcntl.h> #include <fcntl.h>
#if defined(__ANDROID__)
JNIEXPORT void JNICALL Java_org_es_1de_frontend_MainActivity_nativeLogOutput(JNIEnv* jniEnv,
jclass jniClass,
jstring output,
jint logLevel)
{
const char* outputUtf {jniEnv->GetStringUTFChars(output, nullptr)};
LOG(static_cast<LogLevel>(logLevel)) << outputUtf;
jniEnv->ReleaseStringUTFChars(output, outputUtf);
}
#endif
namespace Utils namespace Utils
{ {
namespace Platform namespace Platform
@ -380,10 +392,9 @@ namespace Utils
JNIEnv* jniEnv {reinterpret_cast<JNIEnv*>(SDL_AndroidGetJNIEnv())}; JNIEnv* jniEnv {reinterpret_cast<JNIEnv*>(SDL_AndroidGetJNIEnv())};
jclass jniClass {jniEnv->FindClass("org/es_de/frontend/MainActivity")}; jclass jniClass {jniEnv->FindClass("org/es_de/frontend/MainActivity")};
jmethodID methodID { jmethodID methodID {
jniEnv->GetStaticMethodID(jniClass, "requestStoragePermissions", "()Z")}; jniEnv->GetStaticMethodID(jniClass, "requestStoragePermissions", "()V")};
const bool result { jniEnv->CallStaticVoidMethod(jniClass, methodID);
static_cast<bool>(jniEnv->CallStaticBooleanMethod(jniClass, methodID))}; bool result {false};
// jniEnv->DeleteLocalRef(jniClass);
return result; return result;
} }
@ -461,33 +472,61 @@ namespace Utils
int launchGame(const std::string& packageName, int launchGame(const std::string& packageName,
const std::string& activity, const std::string& activity,
const std::string& action, const std::string& action,
const std::string& fileAsURI, const std::string& category,
std::vector<std::pair<std::string, std::string>>& extras) const std::string& mimeType,
const std::string& data,
const std::string& romRaw,
const std::map<std::string, std::string>& extrasString,
const std::map<std::string, std::string>& extrasBool,
const std::vector<std::string>& activityFlags)
{ {
JNIEnv* jniEnv {reinterpret_cast<JNIEnv*>(SDL_AndroidGetJNIEnv())}; JNIEnv* jniEnv {reinterpret_cast<JNIEnv*>(SDL_AndroidGetJNIEnv())};
jclass jniClass {jniEnv->FindClass("org/es_de/frontend/MainActivity")}; jclass jniClass {jniEnv->FindClass("org/es_de/frontend/MainActivity")};
jmethodID methodID {jniEnv->GetStaticMethodID( jmethodID methodID {jniEnv->GetStaticMethodID(
jniClass, "launchGame", jniClass, "launchGame",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/" "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/"
"String;Ljava/util/Vector;Ljava/util/Vector;)Z")}; "String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/"
jclass vectorClass {jniEnv->FindClass("java/util/Vector")}; "HashMap;Ljava/util/HashMap;Ljava/util/List;)Z")};
jmethodID vectorMID {jniEnv->GetMethodID(vectorClass, "<init>", "()V")};
jmethodID addMethodID { jclass hashMapClass {jniEnv->FindClass("java/util/HashMap")};
jniEnv->GetMethodID(vectorClass, "add", "(Ljava/lang/Object;)Z")}; jmethodID hashMapInit {jniEnv->GetMethodID(hashMapClass, "<init>", "(I)V")};
jobject extrasNames {jniEnv->NewObject(vectorClass, vectorMID)};
jobject extrasValues {jniEnv->NewObject(vectorClass, vectorMID)}; jobject hashMapString {
for (auto& extra : extras) { jniEnv->NewObject(hashMapClass, hashMapInit, extrasString.size())};
jniEnv->CallBooleanMethod(extrasNames, addMethodID, jobject hashMapBool {
jniEnv->NewStringUTF(extra.first.c_str())); jniEnv->NewObject(hashMapClass, hashMapInit, extrasBool.size())};
jniEnv->CallBooleanMethod(extrasValues, addMethodID,
jniEnv->NewStringUTF(extra.second.c_str())); jmethodID hashMapPutMethodString {jniEnv->GetMethodID(
hashMapClass, "put",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;")};
for (auto it : extrasString) {
jniEnv->CallObjectMethod(hashMapString, hashMapPutMethodString,
jniEnv->NewStringUTF(it.first.c_str()),
jniEnv->NewStringUTF(it.second.c_str()));
} }
for (auto it : extrasBool) {
jniEnv->CallObjectMethod(hashMapBool, hashMapPutMethodString,
jniEnv->NewStringUTF(it.first.c_str()),
jniEnv->NewStringUTF(it.second.c_str()));
}
jclass arrayListClass {jniEnv->FindClass("java/util/ArrayList")};
jmethodID arrayListInit {jniEnv->GetMethodID(arrayListClass, "<init>", "()V")};
jmethodID addMethod {
jniEnv->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z")};
jobject flags {jniEnv->NewObject(arrayListClass, arrayListInit)};
for (auto& flag : activityFlags)
jniEnv->CallBooleanMethod(flags, addMethod, jniEnv->NewStringUTF(flag.c_str()));
const bool returnValue {static_cast<bool>(jniEnv->CallStaticBooleanMethod( const bool returnValue {static_cast<bool>(jniEnv->CallStaticBooleanMethod(
jniClass, methodID, jniEnv->NewStringUTF(packageName.c_str()), jniClass, methodID, jniEnv->NewStringUTF(packageName.c_str()),
jniEnv->NewStringUTF(activity.c_str()), jniEnv->NewStringUTF(action.c_str()), jniEnv->NewStringUTF(activity.c_str()), jniEnv->NewStringUTF(action.c_str()),
jniEnv->NewStringUTF(fileAsURI.c_str()), extrasNames, extrasValues))}; jniEnv->NewStringUTF(category.c_str()), jniEnv->NewStringUTF(mimeType.c_str()),
// jniEnv->DeleteLocalRef(vectorClass); jniEnv->NewStringUTF(data.c_str()), jniEnv->NewStringUTF(romRaw.c_str()),
// jniEnv->DeleteLocalRef(jniClass); hashMapString, hashMapBool, flags))};
if (returnValue) if (returnValue)
return -1; return -1;
else else

View file

@ -17,6 +17,12 @@
#if defined(__ANDROID__) #if defined(__ANDROID__)
#include <jni.h> #include <jni.h>
#include <map>
extern "C" void Java_org_es_1de_frontend_MainActivity_nativeLogOutput(JNIEnv* jniEnv,
jclass jniClass,
jstring output,
jint logLevel);
#endif #endif
namespace Utils namespace Utils
@ -68,8 +74,14 @@ namespace Utils
int launchGame(const std::string& packageName, int launchGame(const std::string& packageName,
const std::string& activity, const std::string& activity,
const std::string& action, const std::string& action,
const std::string& fileAsURI, const std::string& category,
std::vector<std::pair<std::string, std::string>>& extras); const std::string& mimeType,
const std::string& data,
const std::string& romRaw,
const std::map<std::string, std::string>& extrasString,
const std::map<std::string, std::string>& extrasBool,
const std::vector<std::string>& activityFlags);
} // namespace Android } // namespace Android
#endif #endif