@@ -1,23 +1,29 @@
package org.dolphinemu.dolphinemu.utils;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;

import com.nononsenseapps.filepicker.FilePickerActivity;
import com.nononsenseapps.filepicker.Utils;

import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.activities.CustomFilePickerActivity;
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public final class FileBrowserHelper
{
@@ -27,6 +33,9 @@
public static final HashSet<String> RAW_EXTENSION = new HashSet<>(Collections.singletonList(
"raw"));

public static final HashSet<String> WAD_EXTENSION = new HashSet<>(Collections.singletonList(
"wad"));

public static void openDirectoryPicker(FragmentActivity activity, HashSet<String> extensions)
{
Intent i = new Intent(activity, CustomFilePickerActivity.class);
@@ -85,4 +94,83 @@ public static String[] getSelectedFiles(Intent result)

return null;
}

public static boolean isPathEmptyOrValid(StringSetting path)
{
return isPathEmptyOrValid(path.getStringGlobal());
}

public static boolean isPathEmptyOrValid(String path)
{
return !path.startsWith("content://") || ContentHandler.exists(path);
}

public static void runAfterExtensionCheck(Context context, Uri uri, Set<String> validExtensions,
Runnable runnable)
{
String extension = null;

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

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

if (extension != null && validExtensions.contains(extension))
{
runnable.run();
return;
}

String message;
if (extension == null)
{
message = context.getString(R.string.no_file_extension);
}
else
{
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));
}

new AlertDialog.Builder(context, R.style.DolphinDialogBase)
.setMessage(message)
.setPositiveButton(R.string.yes, (dialogInterface, i) -> runnable.run())
.setNegativeButton(R.string.no, null)
.setCancelable(false)
.show();
}

@Nullable
private static String getExtension(@Nullable String fileName)
{
if (fileName == null)
return null;

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

// TODO: Replace this with String.join once we can use Java 8
private static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements)
{
StringBuilder sb = new StringBuilder();

boolean first = true;
for (CharSequence element : elements)
{
if (!first)
sb.append(delimiter);
first = false;
sb.append(element);
}

return sb.toString();
}
}
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:attr/selectableItemBackground"/>
<item>
<shape>
<solid android:color="@color/invalid_setting_overlay" />
</shape>
</item>
</layer-list>
@@ -11,4 +11,6 @@

<color name="tv_card_unselected">#444444</color>

<color name="invalid_setting_overlay">#36ff0000</color>

</resources>
@@ -315,6 +315,7 @@
<string name="clear">Clear</string>
<string name="disabled">Disabled</string>
<string name="other">Other</string>
<string name="continue_anyway">Continue Anyway</string>

<!-- Game Grid Screen-->
<string name="add_directory_title">Add Folder to Library</string>
@@ -433,6 +434,12 @@ It can efficiently compress both junk data and encrypted Wii data.

<string name="select_dir">Select This Directory</string>

<!-- File Pickers -->
<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="unavailable_paths">Dolphin does not have permission to access one or more configured paths. Would you like to fix this before starting?</string>

<!-- Misc -->
<string name="pitch">Total Pitch</string>
<string name="yaw">Total Yaw</string>
@@ -10,6 +10,7 @@

#include <jni.h>

#include "Common/Assert.h"
#include "Common/StringUtil.h"
#include "jni/AndroidCommon/IDCache.h"

@@ -42,21 +43,35 @@ std::vector<std::string> JStringArrayToVector(JNIEnv* env, jobjectArray array)
return result;
}

bool IsPathAndroidContent(const std::string& uri)
{
return StringBeginsWith(uri, "content://");
}

std::string OpenModeToAndroid(std::string mode)
{
// The 'b' specifier is not supported. Since we're on POSIX, it's fine to just skip it.
if (!mode.empty() && mode.back() == 'b')
mode.pop_back();

if (mode == "r+")
mode = "rw";
else if (mode == "w+")
mode = "rwt";
else if (mode == "a+")
mode = "rwa";
else if (mode == "a")
mode = "wa";

return mode;
}

int OpenAndroidContent(const std::string& uri, const std::string& mode)
{
JNIEnv* env = IDCache::GetEnvForThread();
const jint fd = env->CallStaticIntMethod(IDCache::GetContentHandlerClass(),
IDCache::GetContentHandlerOpenFd(), ToJString(env, uri),
ToJString(env, mode));

// We can get an IllegalArgumentException when passing an invalid mode
if (env->ExceptionCheck())
{
env->ExceptionDescribe();
abort();
}

return fd;
return env->CallStaticIntMethod(IDCache::GetContentHandlerClass(),
IDCache::GetContentHandlerOpenFd(), ToJString(env, uri),
ToJString(env, mode));
}

bool DeleteAndroidContent(const std::string& uri)
@@ -12,7 +12,16 @@ std::string GetJString(JNIEnv* env, jstring jstr);
jstring ToJString(JNIEnv* env, const std::string& str);
std::vector<std::string> JStringArrayToVector(JNIEnv* env, jobjectArray array);

// Returns true if the given path should be opened as Android content instead of a normal file.
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);

// 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);
int GetNetworkIpAddress();
int GetNetworkPrefixLength();
@@ -18,7 +18,6 @@
#ifdef ANDROID
#include <algorithm>

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

@@ -66,24 +65,17 @@ void IOFile::Swap(IOFile& other) noexcept
bool IOFile::Open(const std::string& filename, const char openmode[])
{
Close();

#ifdef _WIN32
m_good = _tfopen_s(&m_file, UTF8ToTStr(filename).c_str(), UTF8ToTStr(openmode).c_str()) == 0;
#else
#ifdef ANDROID
if (StringBeginsWith(filename, "content://"))
{
// The Java method which OpenAndroidContent passes the mode to does not support the b specifier.
// Since we're on POSIX, it's fine to just remove the b.
std::string mode_without_b(openmode);
mode_without_b.erase(std::remove(mode_without_b.begin(), mode_without_b.end(), 'b'),
mode_without_b.end());
m_file = fdopen(OpenAndroidContent(filename, mode_without_b), mode_without_b.c_str());
}
if (IsPathAndroidContent(filename))
m_file = fdopen(OpenAndroidContent(filename, OpenModeToAndroid(openmode)), openmode);
else
#endif
{
m_file = std::fopen(filename.c_str(), openmode);
}

m_good = m_file != nullptr;
#endif