@@ -2,9 +2,11 @@

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.widget.Toast;

import androidx.appcompat.app.AlertDialog;
@@ -13,10 +15,16 @@
import org.dolphinemu.dolphinemu.BuildConfig;
import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
import org.dolphinemu.dolphinemu.model.GameFileCache;
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner;
import org.dolphinemu.dolphinemu.utils.ContentHandler;
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;

import java.util.Arrays;
import java.util.Set;

public final class MainPresenter
{
@@ -95,18 +103,40 @@ public boolean handleOptionSelection(int itemId, Context context)
return false;
}

public void addDirIfNeeded(Context context)
public void addDirIfNeeded()
{
if (mDirToAdd != null)
{
GameFileCache.addGameFolder(mDirToAdd, context);
GameFileCache.addGameFolder(mDirToAdd);
mDirToAdd = null;
}
}

public void onDirectorySelected(String dir)
public void onDirectorySelected(Intent result)
{
mDirToAdd = dir;
Uri uri = result.getData();

boolean recursive = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.getBooleanGlobal();
String[] childNames = ContentHandler.getChildNames(uri, recursive);
if (Arrays.stream(childNames).noneMatch((name) -> FileBrowserHelper.GAME_EXTENSIONS.contains(
FileBrowserHelper.getExtension(name, false))))
{
AlertDialog.Builder builder = new AlertDialog.Builder(mContext, R.style.DolphinDialogBase);
builder.setMessage(mContext.getString(R.string.wrong_file_extension_in_directory,
FileBrowserHelper.setToSortedDelimitedString(FileBrowserHelper.GAME_EXTENSIONS)));
builder.setPositiveButton(R.string.ok, null);
builder.show();
}

ContentResolver contentResolver = mContext.getContentResolver();
Uri canonicalizedUri = contentResolver.canonicalize(uri);
if (canonicalizedUri != null)
uri = canonicalizedUri;

int takeFlags = result.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION;
mContext.getContentResolver().takePersistableUriPermission(uri, takeFlags);

mDirToAdd = uri.toString();
}

public void installWAD(String file)
@@ -2,6 +2,7 @@

import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;

@@ -39,11 +40,11 @@ public final class TvMainActivity extends FragmentActivity implements MainView
{
private static boolean sShouldRescanLibrary = true;

private MainPresenter mPresenter = new MainPresenter(this, this);
private final MainPresenter mPresenter = new MainPresenter(this, this);

private BrowseSupportFragment mBrowseFragment;

private ArrayList<ArrayObjectAdapter> mGameRows = new ArrayList<>();
private final ArrayList<ArrayObjectAdapter> mGameRows = new ArrayList<>();

@Override
protected void onCreate(Bundle savedInstanceState)
@@ -73,7 +74,7 @@ protected void onResume()
GameFileCacheService.startLoad(this);
}

mPresenter.addDirIfNeeded(this);
mPresenter.addDirIfNeeded();

// In case the user changed a setting that affects how games are displayed,
// such as system language, cover downloading...
@@ -167,14 +168,17 @@ public void launchSettingsActivity(MenuTag menuTag)
@Override
public void launchFileListActivity()
{
FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY);
}

@Override
public void launchOpenFileActivity()
{
FileBrowserHelper.openFilePicker(this, MainPresenter.REQUEST_GAME_FILE, false,
FileBrowserHelper.GAME_EXTENSIONS);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, MainPresenter.REQUEST_GAME_FILE);
}

@Override
@@ -218,19 +222,21 @@ protected void onActivityResult(int requestCode, int resultCode, Intent result)
// If the user picked a file, as opposed to just backing out.
if (resultCode == MainActivity.RESULT_OK)
{
Uri uri = result.getData();
switch (requestCode)
{
case MainPresenter.REQUEST_DIRECTORY:
mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result));
mPresenter.onDirectorySelected(result);
break;

case MainPresenter.REQUEST_GAME_FILE:
EmulationActivity.launch(this, FileBrowserHelper.getSelectedFiles(result));
FileBrowserHelper.runAfterExtensionCheck(this, uri,
FileBrowserHelper.GAME_LIKE_EXTENSIONS,
() -> EmulationActivity.launch(this, result.getData().toString()));
break;

case MainPresenter.REQUEST_WAD_FILE:
FileBrowserHelper.runAfterExtensionCheck(this, result.getData(),
FileBrowserHelper.WAD_EXTENSION,
FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION,
() -> mPresenter.installWAD(result.getData().toString()));
break;
}

Large diffs are not rendered by default.

@@ -28,7 +28,14 @@
public final class FileBrowserHelper
{
public static final HashSet<String> GAME_EXTENSIONS = new HashSet<>(Arrays.asList(
"gcm", "tgc", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "wad", "dol", "elf", "dff"));
"gcm", "tgc", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "wad", "dol", "elf"));

public static final HashSet<String> GAME_LIKE_EXTENSIONS = new HashSet<>(GAME_EXTENSIONS);

static
{
GAME_LIKE_EXTENSIONS.add("dff");
}

public static final HashSet<String> RAW_EXTENSION = new HashSet<>(Collections.singletonList(
"raw"));
@@ -50,21 +57,6 @@ public static void openDirectoryPicker(FragmentActivity activity, HashSet<String
activity.startActivityForResult(i, MainPresenter.REQUEST_DIRECTORY);
}

public static void openFilePicker(FragmentActivity activity, int requestCode, boolean allowMulti,
HashSet<String> extensions)
{
Intent i = new Intent(activity, CustomFilePickerActivity.class);

i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMulti);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE);
i.putExtra(FilePickerActivity.EXTRA_START_PATH,
Environment.getExternalStorageDirectory().getPath());
i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, extensions);

activity.startActivityForResult(i, requestCode);
}

@Nullable
public static String getSelectedPath(Intent result)
{
@@ -79,22 +71,6 @@ public static String getSelectedPath(Intent result)
return null;
}

@Nullable
public static String[] getSelectedFiles(Intent result)
{
// Use the provided utility method to parse the result
List<Uri> files = Utils.getSelectedFilesFromResult(result);
if (!files.isEmpty())
{
String[] paths = new String[files.size()];
for (int i = 0; i < files.size(); i++)
paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath();
return paths;
}

return null;
}

public static boolean isPathEmptyOrValid(StringSetting path)
{
return isPathEmptyOrValid(path.getStringGlobal());
@@ -112,10 +88,10 @@ public static void runAfterExtensionCheck(Context context, Uri uri, Set<String>

String path = uri.getLastPathSegment();
if (path != null)
extension = getExtension(new File(path).getName());
extension = getExtension(new File(path).getName(), false);

if (extension == null)
extension = getExtension(ContentHandler.getDisplayName(uri));
extension = getExtension(ContentHandler.getDisplayName(uri), false);

if (extension != null && validExtensions.contains(extension))
{
@@ -133,10 +109,8 @@ public static void runAfterExtensionCheck(Context context, Uri uri, Set<String>
int messageId = validExtensions.size() == 1 ?
R.string.wrong_file_extension_single : R.string.wrong_file_extension_multiple;

ArrayList<String> extensionsList = new ArrayList<>(validExtensions);
Collections.sort(extensionsList);

message = context.getString(messageId, extension, join(", ", extensionsList));
message = context.getString(messageId, extension,
setToSortedDelimitedString(validExtensions));
}

new AlertDialog.Builder(context, R.style.DolphinDialogBase)
@@ -148,13 +122,22 @@ public static void runAfterExtensionCheck(Context context, Uri uri, Set<String>
}

@Nullable
private static String getExtension(@Nullable String fileName)
public static String getExtension(@Nullable String fileName, boolean includeDot)
{
if (fileName == null)
return null;

int dotIndex = fileName.lastIndexOf(".");
return dotIndex != -1 ? fileName.substring(dotIndex + 1) : null;
if (dotIndex == -1)
return null;
return fileName.substring(dotIndex + (includeDot ? 0 : 1));
}

public static String setToSortedDelimitedString(Set<String> set)
{
ArrayList<String> list = new ArrayList<>(set);
Collections.sort(list);
return join(", ", list);
}

// TODO: Replace this with String.join once we can use Java 8
@@ -438,6 +438,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="no_file_extension">The selected file does not appear to have a file name extension.\n\nContinue anyway?</string>
<string name="wrong_file_extension_single">The selected file has the file name extension \"%1$s\", but \"%2$s\" was expected.\n\nContinue anyway?</string>
<string name="wrong_file_extension_multiple">The selected file has the file name extension \"%1$s\", but one of these extensions was expected: %2$s\n\nContinue anyway?</string>
<string name="wrong_file_extension_in_directory">No compatible files were found in the selected location.\n\nThe supported formats are: %1$s</string>
<string name="unavailable_paths">Dolphin does not have permission to access one or more configured paths. Would you like to fix this before starting?</string>

<!-- Misc -->
@@ -4,6 +4,7 @@

#include "jni/AndroidCommon/AndroidCommon.h"

#include <ios>
#include <string>
#include <string_view>
#include <vector>
@@ -43,6 +44,14 @@ std::vector<std::string> JStringArrayToVector(JNIEnv* env, jobjectArray array)
return result;
}

jobjectArray JStringArrayFromVector(JNIEnv* env, std::vector<std::string> vector)
{
jobjectArray result = env->NewObjectArray(vector.size(), IDCache::GetStringClass(), nullptr);
for (jsize i = 0; i < vector.size(); ++i)
env->SetObjectArrayElement(result, i, ToJString(env, vector[i]));
return result;
}

bool IsPathAndroidContent(const std::string& uri)
{
return StringBeginsWith(uri, "content://");
@@ -66,6 +75,28 @@ std::string OpenModeToAndroid(std::string mode)
return mode;
}

std::string OpenModeToAndroid(std::ios_base::openmode mode)
{
std::string result;

if (mode & std::ios_base::in)
result += 'r';

if (mode & (std::ios_base::out | std::ios_base::app))
result += 'w';

if (mode & std::ios_base::app)
result += 'a';

constexpr std::ios_base::openmode t = std::ios_base::in | std::ios_base::trunc;
if ((mode & t) == t)
result += 't';

// The 'b' specifier is not supported. Since we're on POSIX, it's fine to just skip it.

return result;
}

int OpenAndroidContent(const std::string& uri, const std::string& mode)
{
JNIEnv* env = IDCache::GetEnvForThread();
@@ -81,6 +112,43 @@ bool DeleteAndroidContent(const std::string& uri)
IDCache::GetContentHandlerDelete(), ToJString(env, uri));
}

jlong GetAndroidContentSizeAndIsDirectory(const std::string& uri)
{
JNIEnv* env = IDCache::GetEnvForThread();
return env->CallStaticLongMethod(IDCache::GetContentHandlerClass(),
IDCache::GetContentHandlerGetSizeAndIsDirectory(),
ToJString(env, uri));
}

std::string GetAndroidContentDisplayName(const std::string& uri)
{
JNIEnv* env = IDCache::GetEnvForThread();
jobject display_name =
env->CallStaticObjectMethod(IDCache::GetContentHandlerClass(),
IDCache::GetContentHandlerGetDisplayName(), ToJString(env, uri));
return display_name ? GetJString(env, reinterpret_cast<jstring>(display_name)) : "";
}

std::vector<std::string> GetAndroidContentChildNames(const std::string& uri)
{
JNIEnv* env = IDCache::GetEnvForThread();
jobject children = env->CallStaticObjectMethod(IDCache::GetContentHandlerClass(),
IDCache::GetContentHandlerGetChildNames(),
ToJString(env, uri), false);
return JStringArrayToVector(env, reinterpret_cast<jobjectArray>(children));
}

std::vector<std::string> DoFileSearchAndroidContent(const std::string& directory,
const std::vector<std::string>& extensions,
bool recursive)
{
JNIEnv* env = IDCache::GetEnvForThread();
jobject result = env->CallStaticObjectMethod(
IDCache::GetContentHandlerClass(), IDCache::GetContentHandlerDoFileSearch(),
ToJString(env, directory), JStringArrayFromVector(env, extensions), recursive);
return JStringArrayToVector(env, reinterpret_cast<jobjectArray>(result));
}

int GetNetworkIpAddress()
{
JNIEnv* env = IDCache::GetEnvForThread();
@@ -4,7 +4,9 @@

#pragma once

#include <ios>
#include <string>
#include <vector>

#include <jni.h>

@@ -17,12 +19,29 @@ bool IsPathAndroidContent(const std::string& uri);

// Turns a C/C++ style mode (e.g. "rb") into one which can be used with OpenAndroidContent.
std::string OpenModeToAndroid(std::string mode);
std::string OpenModeToAndroid(std::ios_base::openmode mode);

// Opens a given file and returns a file descriptor.
int OpenAndroidContent(const std::string& uri, const std::string& mode);

// Deletes a given file.
bool DeleteAndroidContent(const std::string& uri);
// Returns -1 if not found, -2 if directory, file size otherwise.
jlong GetAndroidContentSizeAndIsDirectory(const std::string& uri);

// An unmangled URI (one which the C++ code has not appended anything to) can't be relied on
// to contain a file name at all. If a file name is desired, this function is the most reliable
// way to get it, but the display name is not guaranteed to always actually be like a file name.
// An empty string will be returned for files which do not exist.
std::string GetAndroidContentDisplayName(const std::string& uri);

// Returns the display names of all children of a directory, non-recursively.
std::vector<std::string> GetAndroidContentChildNames(const std::string& uri);

std::vector<std::string> DoFileSearchAndroidContent(const std::string& directory,
const std::vector<std::string>& extensions,
bool recursive);

int GetNetworkIpAddress();
int GetNetworkPrefixLength();
int GetNetworkGateway();
@@ -10,6 +10,8 @@ static constexpr jint JNI_VERSION = JNI_VERSION_1_6;

static JavaVM* s_java_vm;

static jclass s_string_class;

static jclass s_native_library_class;
static jmethodID s_display_alert_msg;
static jmethodID s_do_rumble;
@@ -44,6 +46,10 @@ static jmethodID s_compress_cb_run;
static jclass s_content_handler_class;
static jmethodID s_content_handler_open_fd;
static jmethodID s_content_handler_delete;
static jmethodID s_content_handler_get_size_and_is_directory;
static jmethodID s_content_handler_get_display_name;
static jmethodID s_content_handler_get_child_names;
static jmethodID s_content_handler_do_file_search;

static jclass s_network_helper_class;
static jmethodID s_network_helper_get_network_ip_address;
@@ -75,6 +81,11 @@ JNIEnv* GetEnvForThread()
return owned.env;
}

jclass GetStringClass()
{
return s_string_class;
}

jclass GetNativeLibraryClass()
{
return s_native_library_class;
@@ -210,6 +221,26 @@ jmethodID GetContentHandlerDelete()
return s_content_handler_delete;
}

jmethodID GetContentHandlerGetSizeAndIsDirectory()
{
return s_content_handler_get_size_and_is_directory;
}

jmethodID GetContentHandlerGetDisplayName()
{
return s_content_handler_get_display_name;
}

jmethodID GetContentHandlerGetChildNames()
{
return s_content_handler_get_child_names;
}

jmethodID GetContentHandlerDoFileSearch()
{
return s_content_handler_do_file_search;
}

jclass GetNetworkHelperClass()
{
return s_network_helper_class;
@@ -229,6 +260,7 @@ jmethodID GetNetworkHelperGetNetworkGateway()
{
return s_network_helper_get_network_gateway;
}

} // namespace IDCache

#ifdef __cplusplus
@@ -243,6 +275,9 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved)
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK)
return JNI_ERR;

const jclass string_class = env->FindClass("java/lang/String");
s_string_class = reinterpret_cast<jclass>(env->NewGlobalRef(string_class));

const jclass native_library_class = env->FindClass("org/dolphinemu/dolphinemu/NativeLibrary");
s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class));
s_display_alert_msg = env->GetStaticMethodID(s_native_library_class, "displayAlertMsg",
@@ -306,6 +341,15 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved)
"(Ljava/lang/String;Ljava/lang/String;)I");
s_content_handler_delete =
env->GetStaticMethodID(s_content_handler_class, "delete", "(Ljava/lang/String;)Z");
s_content_handler_get_size_and_is_directory = env->GetStaticMethodID(
s_content_handler_class, "getSizeAndIsDirectory", "(Ljava/lang/String;)J");
s_content_handler_get_display_name = env->GetStaticMethodID(
s_content_handler_class, "getDisplayName", "(Ljava/lang/String;)Ljava/lang/String;");
s_content_handler_get_child_names = env->GetStaticMethodID(
s_content_handler_class, "getChildNames", "(Ljava/lang/String;Z)[Ljava/lang/String;");
s_content_handler_do_file_search =
env->GetStaticMethodID(s_content_handler_class, "doFileSearch",
"(Ljava/lang/String;[Ljava/lang/String;Z)[Ljava/lang/String;");

const jclass network_helper_class =
env->FindClass("org/dolphinemu/dolphinemu/utils/NetworkHelper");
@@ -10,6 +10,8 @@ namespace IDCache
{
JNIEnv* GetEnvForThread();

jclass GetStringClass();

jclass GetNativeLibraryClass();
jmethodID GetDisplayAlertMsg();
jmethodID GetDoRumble();
@@ -44,6 +46,10 @@ jmethodID GetCompressCallbackRun();
jclass GetContentHandlerClass();
jmethodID GetContentHandlerOpenFd();
jmethodID GetContentHandlerDelete();
jmethodID GetContentHandlerGetSizeAndIsDirectory();
jmethodID GetContentHandlerGetDisplayName();
jmethodID GetContentHandlerGetChildNames();
jmethodID GetContentHandlerDoFileSearch();

jclass GetNetworkHelperClass();
jmethodID GetNetworkHelperGetNetworkIpAddress();
@@ -4,6 +4,7 @@

#include <algorithm>
#include <functional>
#include <iterator>

#include "Common/CommonPaths.h"
#include "Common/FileSearch.h"
@@ -15,6 +16,10 @@
namespace fs = std::filesystem;
#define HAS_STD_FILESYSTEM
#else
#ifdef ANDROID
#include "jni/AndroidCommon/AndroidCommon.h"
#endif

#include <cstring>
#include "Common/CommonFuncs.h"
#include "Common/FileUtil.h"
@@ -24,36 +29,30 @@ namespace Common
{
#ifndef HAS_STD_FILESYSTEM

static std::vector<std::string>
FileSearchWithTest(const std::vector<std::string>& directories, bool recursive,
std::function<bool(const File::FSTEntry&)> callback)
static void FileSearchWithTest(const std::string& directory, bool recursive,
std::vector<std::string>* result_out,
std::function<bool(const File::FSTEntry&)> callback)
{
std::vector<std::string> result;
for (const std::string& directory : directories)
{
File::FSTEntry top = File::ScanDirectoryTree(directory, recursive);

std::function<void(File::FSTEntry&)> DoEntry;
DoEntry = [&](File::FSTEntry& entry) {
if (callback(entry))
result.push_back(entry.physicalName);
for (auto& child : entry.children)
DoEntry(child);
};
for (auto& child : top.children)
File::FSTEntry top = File::ScanDirectoryTree(directory, recursive);

const std::function<void(File::FSTEntry&)> DoEntry = [&](File::FSTEntry& entry) {
if (callback(entry))
result_out->push_back(entry.physicalName);
for (auto& child : entry.children)
DoEntry(child);
}
// remove duplicates
std::sort(result.begin(), result.end());
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
};

for (auto& child : top.children)
DoEntry(child);
}

std::vector<std::string> DoFileSearch(const std::vector<std::string>& directories,
const std::vector<std::string>& exts, bool recursive)
{
std::vector<std::string> result;

bool accept_all = exts.empty();
return FileSearchWithTest(directories, recursive, [&](const File::FSTEntry& entry) {
const auto callback = [&exts, accept_all](const File::FSTEntry& entry) {
if (accept_all)
return true;
if (entry.isDirectory)
@@ -63,7 +62,34 @@ std::vector<std::string> DoFileSearch(const std::vector<std::string>& directorie
return name.length() >= ext.length() &&
strcasecmp(name.c_str() + name.length() - ext.length(), ext.c_str()) == 0;
});
});
};

for (const std::string& directory : directories)
{
#ifdef ANDROID
// While File::ScanDirectoryTree (which is called in FileSearchWithTest) does handle Android
// content correctly, having a specialized implementation of DoFileSearch for Android content
// provides a much needed performance boost. Also, this specialized implementation will be
// required if we in the future replace the use of File::ScanDirectoryTree with std::filesystem.
if (IsPathAndroidContent(directory))
{
const std::vector<std::string> partial_result =
DoFileSearchAndroidContent(directory, exts, recursive);

result.insert(result.end(), std::make_move_iterator(partial_result.begin()),
std::make_move_iterator(partial_result.end()));
}
else
#endif
{
FileSearchWithTest(directory, recursive, &result, callback);
}
}

// remove duplicates
std::sort(result.begin(), result.end());
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
}

#else
@@ -78,19 +78,40 @@ FileInfo::FileInfo(const char* path) : FileInfo(std::string(path))
#else
FileInfo::FileInfo(const std::string& path) : FileInfo(path.c_str())
{
#ifdef ANDROID
if (IsPathAndroidContent(path))
AndroidContentInit(path);
else
#endif
m_exists = stat(path.c_str(), &m_stat) == 0;
}

FileInfo::FileInfo(const char* path)
{
m_exists = stat(path, &m_stat) == 0;
#ifdef ANDROID
if (IsPathAndroidContent(path))
AndroidContentInit(path);
else
#endif
m_exists = stat(path, &m_stat) == 0;
}
#endif

FileInfo::FileInfo(int fd)
{
m_exists = fstat(fd, &m_stat);
m_exists = fstat(fd, &m_stat) == 0;
}

#ifdef ANDROID
void FileInfo::AndroidContentInit(const std::string& path)
{
const jlong result = GetAndroidContentSizeAndIsDirectory(path);
m_exists = result != -1;
m_stat.st_mode = result == -2 ? S_IFDIR : S_IFREG;
m_stat.st_size = result >= 0 ? result : 0;
}
#endif

bool FileInfo::Exists() const
{
return m_exists;
@@ -476,14 +497,47 @@ FSTEntry ScanDirectoryTree(const std::string& directory, bool recursive)
{
const std::string virtual_name(TStrToUTF8(ffd.cFileName));
#else
DIR* dirp = opendir(directory.c_str());
if (!dirp)
return parent_entry;
DIR* dirp = nullptr;

#ifdef ANDROID
std::vector<std::string> child_names;
if (IsPathAndroidContent(directory))
{
child_names = GetAndroidContentChildNames(directory);
}
else
#endif
{
dirp = opendir(directory.c_str());
if (!dirp)
return parent_entry;
}

#ifdef ANDROID
auto it = child_names.cbegin();
#endif

// non Windows loop
while (dirent* result = readdir(dirp))
while (true)
{
const std::string virtual_name(result->d_name);
std::string virtual_name;

#ifdef ANDROID
if (!dirp)
{
if (it == child_names.cend())
break;
virtual_name = *it;
++it;
}
else
#endif
{
dirent* result = readdir(dirp);
if (!result)
break;
virtual_name = result->d_name;
}
#endif
if (virtual_name == "." || virtual_name == "..")
continue;
@@ -514,7 +568,8 @@ FSTEntry ScanDirectoryTree(const std::string& directory, bool recursive)
FindClose(hFind);
#else
}
closedir(dirp);
if (dirp)
closedir(dirp);
#endif

return parent_entry;
@@ -18,6 +18,11 @@
#include "Common/StringUtil.h"
#endif

#ifdef ANDROID
#include "Common/StringUtil.h"
#include "jni/AndroidCommon/AndroidCommon.h"
#endif

// User directory indices for GetUserPath
enum
{
@@ -109,6 +114,10 @@ class FileInfo final
u64 GetSize() const;

private:
#ifdef ANDROID
void AndroidContentInit(const std::string& path);
#endif

struct stat m_stat;
bool m_exists;
};
@@ -214,14 +223,20 @@ std::string GetExeDirectory();
bool WriteStringToFile(const std::string& filename, std::string_view str);
bool ReadFileToString(const std::string& filename, std::string& str);

// To deal with Windows being dumb at unicode:
// To deal with Windows not fully supporting UTF-8 and Android not fully supporting paths.
template <typename T>
void OpenFStream(T& fstream, const std::string& filename, std::ios_base::openmode openmode)
{
#ifdef _WIN32
fstream.open(UTF8ToTStr(filename).c_str(), openmode);
#else
fstream.open(filename.c_str(), openmode);
#ifdef ANDROID
// Unfortunately it seems like the non-standard __open is the only way to use a file descriptor
if (IsPathAndroidContent(filename))
fstream.__open(OpenAndroidContent(filename, OpenModeToAndroid(openmode)), openmode);
else
#endif
fstream.open(filename.c_str(), openmode);
#endif
}

@@ -158,6 +158,15 @@ BootParameters::GenerateFromFile(std::vector<std::string> paths,
if (paths.size() == 1)
paths.clear();

#ifdef ANDROID
if (extension.empty() && IsPathAndroidContent(path))
{
const std::string display_name = GetAndroidContentDisplayName(path);
SplitPath(display_name, nullptr, nullptr, &extension);
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
}
#endif

static const std::unordered_set<std::string> disc_image_extensions = {
{".gcm", ".iso", ".tgc", ".wbfs", ".ciso", ".gcz", ".wia", ".rvz", ".dol", ".elf"}};
if (disc_image_extensions.find(extension) != disc_image_extensions.end() || is_drive)