Large diffs are not rendered by default.

@@ -38,6 +38,8 @@ private GameFile(long pointer)

public native int getRevision();

public native int getBlobType();

public native String getBlobTypeString();

public native long getBlockSize();
@@ -46,8 +48,12 @@ private GameFile(long pointer)

public native boolean shouldShowFileFormatDetails();

public native boolean shouldAllowConversion();

public native long getFileSize();

public native boolean isDatelDisc();

public native int[] getBanner();

public native int getBannerWidth();
@@ -0,0 +1,40 @@
package org.dolphinemu.dolphinemu.utils;

import androidx.appcompat.app.AlertDialog;

import android.content.Context;
import android.content.DialogInterface.OnClickListener;

import java.util.ArrayList;

public class AlertDialogItemsBuilder
{
private Context mContext;

private ArrayList<CharSequence> mLabels = new ArrayList<>();
private ArrayList<OnClickListener> mListeners = new ArrayList<>();

public AlertDialogItemsBuilder(Context context)
{
mContext = context;
}

public void add(int stringId, OnClickListener listener)
{
mLabels.add(mContext.getResources().getString(stringId));
mListeners.add(listener);
}

public void add(CharSequence label, OnClickListener listener)
{
mLabels.add(label);
mListeners.add(listener);
}

public void applyToBuilder(AlertDialog.Builder builder)
{
CharSequence[] labels = new CharSequence[mLabels.size()];
labels = mLabels.toArray(labels);
builder.setItems(labels, (dialog, i) -> mListeners.get(i).onClick(dialog, i));
}
}
@@ -0,0 +1,6 @@
package org.dolphinemu.dolphinemu.utils;

public interface CompressCallback
{
boolean run(String text, float completion);
}
@@ -0,0 +1,39 @@
package org.dolphinemu.dolphinemu.utils;

import android.content.ContentResolver;
import android.net.Uri;
import android.provider.DocumentsContract;

import org.dolphinemu.dolphinemu.DolphinApplication;

import java.io.FileNotFoundException;

public class ContentHandler
{
public static int openFd(String uri, String mode)
{
try
{
return DolphinApplication.getAppContext().getContentResolver()
.openFileDescriptor(Uri.parse(uri), mode).detachFd();
}
catch (FileNotFoundException | NullPointerException e)
{
return -1;
}
}

public static boolean delete(String uri)
{
try
{
ContentResolver resolver = DolphinApplication.getAppContext().getContentResolver();
return DocumentsContract.deleteDocument(resolver, Uri.parse(uri));
}
catch (FileNotFoundException e)
{
// Return true because we care about the file not being there, not the actual delete.
return true;
}
}
}
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
xmlns:app="http://schemas.android.com/apk/res-auto">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">

<TextView
android:id="@+id/label_format_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/convert_format_info"
app:layout_constraintWidth_max="400dp"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/divider" />

<View
android:id="@+id/divider"
android:layout_width="1dp"
android:layout_height="0dp"
android:background="#1F000000"
android:layout_marginStart="24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/label_format_info"
app:layout_constraintEnd_toStartOf="@id/fragment_convert" />

<FrameLayout
android:id="@+id/fragment_convert"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
app:layout_constraintWidth_max="400dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/divider"
app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

</ScrollView>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
xmlns:app="http://schemas.android.com/apk/res-auto">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">

<FrameLayout
android:id="@+id/fragment_convert"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintWidth_max="400dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/divider" />

<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="#1F000000"
android:layout_marginTop="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/fragment_convert"
app:layout_constraintBottom_toTopOf="@id/label_format_info" />

<TextView
android:id="@+id/label_format_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/convert_format_info"
app:layout_constraintWidth_max="400dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

</ScrollView>
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">

<TextView
android:id="@+id/label_format"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:text="@string/convert_format"
app:layout_constraintTop_toTopOf="@id/spinner_format"
app:layout_constraintBottom_toBottomOf="@id/spinner_format"
app:layout_constraintStart_toStartOf="parent" />

<TextView
android:id="@+id/label_block_size"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:text="@string/convert_block_size"
app:layout_constraintTop_toTopOf="@id/spinner_block_size"
app:layout_constraintBottom_toBottomOf="@id/spinner_block_size"
app:layout_constraintStart_toStartOf="parent" />

<TextView
android:id="@+id/label_compression"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:text="@string/convert_compression"
app:layout_constraintTop_toTopOf="@id/spinner_compression"
app:layout_constraintBottom_toBottomOf="@id/spinner_compression"
app:layout_constraintStart_toStartOf="parent" />

<TextView
android:id="@+id/label_compression_level"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:text="@string/convert_compression_level"
app:layout_constraintTop_toTopOf="@id/spinner_compression_level"
app:layout_constraintBottom_toBottomOf="@id/spinner_compression_level"
app:layout_constraintStart_toStartOf="parent" />

<TextView
android:id="@+id/label_remove_junk_data"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:text="@string/convert_remove_junk_data"
app:layout_constraintTop_toTopOf="@id/checkbox_remove_junk_data"
app:layout_constraintBottom_toBottomOf="@id/checkbox_remove_junk_data"
app:layout_constraintStart_toStartOf="parent" />

<androidx.constraintlayout.widget.Barrier
android:id="@+id/label_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="label_format,label_block_size,label_compression,label_compression_level" />

<Spinner
android:id="@+id/spinner_format"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
app:layout_constraintStart_toEndOf="@id/label_barrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Spinner
android:id="@+id/spinner_block_size"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:gravity="end"
app:layout_constraintStart_toEndOf="@id/label_barrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/spinner_format" />

<Spinner
android:id="@+id/spinner_compression"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:gravity="end"
app:layout_constraintStart_toEndOf="@id/label_barrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/spinner_block_size" />

<Spinner
android:id="@+id/spinner_compression_level"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:gravity="end"
app:layout_constraintStart_toEndOf="@id/label_barrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/spinner_compression" />

<CheckBox
android:id="@+id/checkbox_remove_junk_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="16dp"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@id/label_remove_junk_data"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/spinner_compression_level" />

<Button
android:id="@+id/button_convert"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/convert_convert"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/checkbox_remove_junk_data" />

</androidx.constraintlayout.widget.ConstraintLayout>
@@ -433,24 +433,6 @@
<item>Classic A</item>
</string-array>

<string-array name="gameSettingsMenusGC">
<item>Details</item>
<item>Set as Default ISO</item>
<item>Core Settings</item>
<item>GFX Settings</item>
<item>GameCube Controller Settings</item>
<item>Clear Game Settings</item>
</string-array>
<string-array name="gameSettingsMenusWii">
<item>Details</item>
<item>Set as Default ISO</item>
<item>Core Settings</item>
<item>GFX Settings</item>
<item>GameCube Controller Settings</item>
<item>Wii Controller Settings</item>
<item>Clear Game Settings</item>
</string-array>

<string-array name="orientationEntries">
<item>Landscape</item>
<item>Portrait</item>
@@ -467,4 +449,147 @@
<item>Use Device Sensors (Without Pointer Emulation)</item>
<item>Don\'t Use Device Sensors</item>
</string-array>

<string-array name="convertFormatEntries" translatable="false">
<item>ISO</item>
<item>GCZ</item>
<item>WIA</item>
<item>RVZ</item>
</string-array>
<integer-array name="convertFormatValues">
<item>0</item>
<item>3</item>
<item>7</item>
<item>8</item>
</integer-array>

<string-array name="convertBlockSizeGczEntries">
<item>32 KiB</item>
</string-array>
<integer-array name="convertBlockSizeGczValues">
<item>32768</item>
</integer-array>

<string-array name="convertBlockSizeWiaEntries">
<item>2 MiB</item>
</string-array>
<integer-array name="convertBlockSizeWiaValues">
<item>2097152</item>
</integer-array>

<string-array name="convertBlockSizeRvzEntries">
<item>32 KiB</item>
<item>64 KiB</item>
<item>128 KiB</item>
<item>256 KiB</item>
<item>512 KiB</item>
<item>1 MiB</item>
<item>2 MiB</item>
</string-array>
<integer-array name="convertBlockSizeRvzValues">
<item>32768</item>
<item>65536</item>
<item>131072</item>
<item>262144</item>
<item>524288</item>
<item>1048576</item>
<item>2097152</item>
</integer-array>

<string-array name="convertCompressionGczEntries" translatable="false">
<item>Deflate</item>
</string-array>
<integer-array name="convertCompressionGczValues">
<item>0</item>
</integer-array>

<string-array name="convertCompressionWiaEntries">
<item>No Compression</item>
<item>Purge</item>
<item>bzip2 (slow)</item>
<item>LZMA (slow)</item>
<item>LZMA2 (slow)</item>
</string-array>
<integer-array name="convertCompressionWiaValues">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
</integer-array>

<string-array name="convertCompressionRvzEntries">
<item>No Compression</item>
<item>bzip2 (slow)</item>
<item>LZMA (slow)</item>
<item>LZMA2 (slow)</item>
<item>Zstandard (recommended)</item>
</string-array>
<integer-array name="convertCompressionRvzValues">
<item>0</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
</integer-array>

<string-array name="convertCompressionLevelEntries">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
</string-array>
<integer-array name="convertCompressionLevelValues">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
</integer-array>

<string-array name="convertCompressionLevelZstdEntries">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
<item>10</item>
<item>11</item>
<item>12</item>
<item>13</item>
<item>14</item>
<item>15</item>
<item>16</item>
<item>17</item>
<item>18</item>
<item>19</item>
<item>20</item>
<item>21</item>
<item>22</item>
</string-array>
<integer-array name="convertCompressionLevelZstdValues">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
</integer-array>
</resources>
@@ -324,7 +324,15 @@
<string name="wad_install_success">Successfully installed this title to the NAND.</string>
<string name="wad_install_failure">Failed to install this title to the NAND.</string>

<!-- Preferences Screen -->
<!-- Game Properties Screen -->
<string name="properties_details">Details</string>
<string name="properties_convert">Convert File</string>
<string name="properties_set_default_iso">Set as Default ISO</string>
<string name="properties_core_settings">Core Settings</string>
<string name="properties_gfx_settings">GFX Settings</string>
<string name="properties_gc_controller">GameCube Controller Settings</string>
<string name="properties_wii_controller">Wii Controller Settings</string>
<string name="properties_clear_game_settings">Clear Game Settings</string>
<string name="preferences_save_exit">Save and Exit</string>
<string name="preferences_settings">Settings</string>
<string name="preferences_game_properties">Game Properties</string>
@@ -341,6 +349,30 @@
<string name="game_details_block_size">Block Size</string>
<string name="game_details_no_compression">No Compression</string>

<!-- Convert Screen -->
<string name="convert_format">Format</string>
<string name="convert_block_size">Block Size</string>
<string name="convert_compression">Compression</string>
<string name="convert_compression_level">Compression Level</string>
<string name="convert_remove_junk_data">Remove Junk Data (Irreversible)</string>
<string name="convert_convert">Convert</string>
<string name="convert_converting">Converting</string>
<string name="convert_warning_iso">Removing junk data does not save any space when converting to ISO (unless you package the ISO file in a compressed file format such as ZIP afterwards). Do you want to continue anyway?</string>
<string name="convert_warning_gcz">Converting Wii disc images to GCZ without removing junk data does not save any noticeable amount of space compared to converting to ISO. Do you want to continue anyway?</string>
<string name="convert_success_message">The disc image was successfully converted.</string>
<string name="convert_failure_message">Dolphin failed to complete the requested action.</string>
<string name="convert_format_info">
ISO: A simple and robust format which is supported by many programs. It takes up more space
than any other format.
\n\nGCZ: A basic compressed format which is compatible with most versions of Dolphin and some
other programs. It can\'t efficiently compress junk data (unless removed) or encrypted Wii data.
\n\nWIA: An advanced compressed format which is compatible with Dolphin 5.0-12188 and later,
and a few other programs. It can efficiently compress encrypted Wii data, but not junk data
(unless removed).
\n\nRVZ: An advanced compressed format which is compatible with Dolphin 5.0-12188 and later.
It can efficiently compress both junk data and encrypted Wii data.
</string>

<!-- Emulation Menu -->
<string name="pause_emulation">Pause Emulation</string>
<string name="unpause_emulation">Unpause Emulation</string>
@@ -11,6 +11,7 @@
#include <jni.h>

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

std::string GetJString(JNIEnv* env, jstring jstr)
{
@@ -40,3 +41,27 @@ std::vector<std::string> JStringArrayToVector(JNIEnv* env, jobjectArray array)

return result;
}

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;
}

bool DeleteAndroidContent(const std::string& uri)
{
JNIEnv* env = IDCache::GetEnvForThread();
return env->CallStaticBooleanMethod(IDCache::GetContentHandlerClass(),
IDCache::GetContentHandlerDelete(), ToJString(env, uri));
}
@@ -11,3 +11,6 @@
std::string GetJString(JNIEnv* env, jstring jstr);
jstring ToJString(JNIEnv* env, const std::string& str);
std::vector<std::string> JStringArrayToVector(JNIEnv* env, jobjectArray array);

int OpenAndroidContent(const std::string& uri, const std::string& mode);
bool DeleteAndroidContent(const std::string& uri);
@@ -0,0 +1,15 @@
add_library(androidcommon STATIC
AndroidCommon.cpp
AndroidCommon.h
IDCache.cpp
IDCache.h
)

target_link_libraries(androidcommon
PRIVATE
android
log
"-Wl,--no-warn-mismatch"
"-Wl,--whole-archive"
"-Wl,--no-whole-archive"
)
@@ -36,6 +36,13 @@ static jclass s_ini_file_section_class;
static jfieldID s_ini_file_section_pointer;
static jmethodID s_ini_file_section_constructor;

static jclass s_compress_cb_class;
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;

namespace IDCache
{
JNIEnv* GetEnvForThread()
@@ -161,6 +168,31 @@ jmethodID GetIniFileSectionConstructor()
return s_ini_file_section_constructor;
}

jclass GetCompressCallbackClass()
{
return s_compress_cb_class;
}

jmethodID GetCompressCallbackRun()
{
return s_compress_cb_run;
}

jclass GetContentHandlerClass()
{
return s_content_handler_class;
}

jmethodID GetContentHandlerOpenFd()
{
return s_content_handler_open_fd;
}

jmethodID GetContentHandlerDelete()
{
return s_content_handler_delete;
}

} // namespace IDCache

#ifdef __cplusplus
@@ -223,6 +255,19 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved)
s_linked_hash_map_put = env->GetMethodID(
s_linked_hash_map_class, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

const jclass compress_cb_class =
env->FindClass("org/dolphinemu/dolphinemu/utils/CompressCallback");
s_compress_cb_class = reinterpret_cast<jclass>(env->NewGlobalRef(compress_cb_class));
s_compress_cb_run = env->GetMethodID(s_compress_cb_class, "run", "(Ljava/lang/String;F)Z");

const jclass content_handler_class =
env->FindClass("org/dolphinemu/dolphinemu/utils/ContentHandler");
s_content_handler_class = reinterpret_cast<jclass>(env->NewGlobalRef(content_handler_class));
s_content_handler_open_fd = env->GetStaticMethodID(s_content_handler_class, "openFd",
"(Ljava/lang/String;Ljava/lang/String;)I");
s_content_handler_delete =
env->GetStaticMethodID(s_content_handler_class, "delete", "(Ljava/lang/String;)Z");

return JNI_VERSION;
}

@@ -239,6 +284,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved)
env->DeleteGlobalRef(s_linked_hash_map_class);
env->DeleteGlobalRef(s_ini_file_class);
env->DeleteGlobalRef(s_ini_file_section_class);
env->DeleteGlobalRef(s_compress_cb_class);
env->DeleteGlobalRef(s_content_handler_class);
}

#ifdef __cplusplus
@@ -38,4 +38,11 @@ jclass GetIniFileSectionClass();
jfieldID GetIniFileSectionPointer();
jmethodID GetIniFileSectionConstructor();

jclass GetCompressCallbackClass();
jmethodID GetCompressCallbackRun();

jclass GetContentHandlerClass();
jmethodID GetContentHandlerOpenFd();
jmethodID GetContentHandlerDelete();

} // namespace IDCache
@@ -1,7 +1,6 @@
add_library(main SHARED
AndroidCommon/AndroidCommon.cpp
AndroidCommon/IDCache.cpp
GameList/GameFile.cpp
GameList/GameFile.h
GameList/GameFileCache.cpp
IniFile.cpp
MainAndroid.cpp
@@ -10,6 +9,7 @@ add_library(main SHARED

target_link_libraries(main
PRIVATE
androidcommon
core
uicommon
)
@@ -32,3 +32,5 @@ file(REMOVE_RECURSE ${CMAKE_SOURCE_DIR}/Source/Android/app/src/main/assets/Sys/R
file(REMOVE_RECURSE ${CMAKE_SOURCE_DIR}/Source/Android/app/src/main/assets/Sys/Themes/)

set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} main)

add_subdirectory(AndroidCommon)
@@ -63,6 +63,8 @@ JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getDiscNumb
jobject obj);
JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getRevision(JNIEnv* env,
jobject obj);
JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBlobType(JNIEnv* env,
jobject obj);
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_model_GameFile_getBlobTypeString(JNIEnv* env, jobject obj);
JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBlockSize(JNIEnv* env,
@@ -71,8 +73,12 @@ JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_model_GameFile_getCompressionMethod(JNIEnv* env, jobject obj);
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_model_GameFile_shouldShowFileFormatDetails(JNIEnv* env, jobject obj);
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_model_GameFile_shouldAllowConversion(JNIEnv* env, jobject obj);
JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getFileSize(JNIEnv* env,
jobject obj);
JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_isDatelDisc(JNIEnv* env,
jobject obj);
JNIEXPORT jintArray JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBanner(JNIEnv* env,
jobject obj);
JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBannerWidth(JNIEnv* env,
@@ -154,6 +160,12 @@ JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getRevision
return GetRef(env, obj)->GetRevision();
}

JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBlobType(JNIEnv* env,
jobject obj)
{
return static_cast<jint>(GetRef(env, obj)->GetBlobType());
}

JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_model_GameFile_getBlobTypeString(JNIEnv* env, jobject obj)
{
@@ -175,7 +187,13 @@ Java_org_dolphinemu_dolphinemu_model_GameFile_getCompressionMethod(JNIEnv* env,
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_model_GameFile_shouldShowFileFormatDetails(JNIEnv* env, jobject obj)
{
return GetRef(env, obj)->ShouldShowFileFormatDetails();
return static_cast<jboolean>(GetRef(env, obj)->ShouldShowFileFormatDetails());
}

JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_model_GameFile_shouldAllowConversion(JNIEnv* env, jobject obj)
{
return static_cast<jboolean>(GetRef(env, obj)->ShouldAllowConversion());
}

JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getFileSize(JNIEnv* env,
@@ -184,6 +202,12 @@ JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getFileSiz
return GetRef(env, obj)->GetFileSize();
}

JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_isDatelDisc(JNIEnv* env,
jobject obj)
{
return static_cast<jboolean>(GetRef(env, obj)->IsDatelDisc());
}

JNIEXPORT jintArray JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBanner(JNIEnv* env,
jobject obj)
{
@@ -18,6 +18,7 @@
#include <utility>

#include "Common/AndroidAnalytics.h"
#include "Common/Assert.h"
#include "Common/CPUDetect.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
@@ -26,6 +27,7 @@
#include "Common/IniFile.h"
#include "Common/Logging/LogManager.h"
#include "Common/MsgHandler.h"
#include "Common/ScopeGuard.h"
#include "Common/Version.h"
#include "Common/WindowSystemInfo.h"

@@ -45,7 +47,9 @@
#include "Core/State.h"
#include "Core/WiiUtils.h"

#include "DiscIO/Blob.h"
#include "DiscIO/Enums.h"
#include "DiscIO/ScrubbedBlob.h"
#include "DiscIO/Volume.h"

#include "InputCommon/ControllerInterface/Android/Android.h"
@@ -663,6 +667,65 @@ JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_InstallW
return static_cast<jboolean>(WiiUtils::InstallWAD(path));
}

JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ConvertDiscImage(
JNIEnv* env, jobject obj, jstring jInPath, jstring jOutPath, jint jPlatform, jint jFormat,
jint jBlockSize, jint jCompression, jint jCompressionLevel, jboolean jScrub, jobject jCallback)
{
const std::string in_path = GetJString(env, jInPath);
const std::string out_path = GetJString(env, jOutPath);
const DiscIO::Platform platform = static_cast<DiscIO::Platform>(jPlatform);
const DiscIO::BlobType format = static_cast<DiscIO::BlobType>(jFormat);
const DiscIO::WIARVZCompressionType compression =
static_cast<DiscIO::WIARVZCompressionType>(jCompression);
const bool scrub = static_cast<bool>(jScrub);

std::unique_ptr<DiscIO::BlobReader> blob_reader;
if (scrub)
blob_reader = DiscIO::ScrubbedBlob::Create(in_path);
else
blob_reader = DiscIO::CreateBlobReader(in_path);

if (!blob_reader)
return static_cast<jboolean>(false);

jobject jCallbackGlobal = env->NewGlobalRef(jCallback);
Common::ScopeGuard scope_guard([jCallbackGlobal, env] { env->DeleteGlobalRef(jCallbackGlobal); });

const auto callback = [&jCallbackGlobal](const std::string& text, float completion) {
JNIEnv* env = IDCache::GetEnvForThread();
return static_cast<bool>(env->CallBooleanMethod(
jCallbackGlobal, IDCache::GetCompressCallbackRun(), ToJString(env, text), completion));
};

bool success = false;

switch (format)
{
case DiscIO::BlobType::PLAIN:
success = DiscIO::ConvertToPlain(blob_reader.get(), in_path, out_path, callback);
break;

case DiscIO::BlobType::GCZ:
success =
DiscIO::ConvertToGCZ(blob_reader.get(), in_path, out_path,
platform == DiscIO::Platform::WiiDisc ? 1 : 0, jBlockSize, callback);
break;

case DiscIO::BlobType::WIA:
case DiscIO::BlobType::RVZ:
success = DiscIO::ConvertToWIAOrRVZ(blob_reader.get(), in_path, out_path,
format == DiscIO::BlobType::RVZ, compression,
jCompressionLevel, jBlockSize, callback);
break;

default:
ASSERT(false);
break;
}

return static_cast<jboolean>(success);
}

JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_FormatSize(JNIEnv* env,
jobject obj,
jlong bytes,
@@ -64,8 +64,8 @@ if (MSVC)
add_definitions(/I${PCH_DIRECTORY})
add_definitions(/Yu${PCH_PATH})

# Don't include timestamps in binaries
add_link_options(/Brepro)
# Don't include timestamps in binaries
add_link_options(/Brepro)
endif()

# These aren't actually needed for C11/C++11
@@ -164,6 +164,11 @@ elseif(WIN32)
if (_M_X86_64)
target_link_libraries(common PRIVATE opengl32.lib)
endif()
elseif (ANDROID)
target_link_libraries(common
PRIVATE
androidcommon
)
endif()

if(ANDROID)
@@ -15,6 +15,13 @@
#include <unistd.h>
#endif

#ifdef ANDROID
#include <algorithm>

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

#include "Common/CommonTypes.h"
#include "Common/File.h"
#include "Common/FileUtil.h"
@@ -62,7 +69,21 @@ bool IOFile::Open(const std::string& filename, const char openmode[])
#ifdef _WIN32
m_good = _tfopen_s(&m_file, UTF8ToTStr(filename).c_str(), UTF8ToTStr(openmode).c_str()) == 0;
#else
m_file = std::fopen(filename.c_str(), openmode);
#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());
}
else
#endif
{
m_file = std::fopen(filename.c_str(), openmode);
}
m_good = m_file != nullptr;
#endif

@@ -46,6 +46,11 @@
#include <sys/param.h>
#endif

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

#ifndef S_ISDIR
#define S_ISDIR(m) (((m)&S_IFMT) == S_IFDIR)
#endif
@@ -132,10 +137,19 @@ bool Delete(const std::string& filename)
{
INFO_LOG(COMMON, "Delete: file %s", filename.c_str());

#ifdef ANDROID
if (StringBeginsWith(filename, "content://"))
{
const bool success = DeleteAndroidContent(filename);
if (!success)
WARN_LOG(COMMON, "Delete failed on %s", filename.c_str());
return success;
}
#endif

const FileInfo file_info(filename);

// Return true because we care about the file no
// being there, not the actual delete.
// Return true because we care about the file not being there, not the actual delete.
if (!file_info.Exists())
{
WARN_LOG(COMMON, "Delete: %s does not exist", filename.c_str());
@@ -15,6 +15,7 @@
// automatically do the right thing.

#include <array>
#include <functional>
#include <memory>
#include <optional>
#include <string>
@@ -175,17 +176,16 @@ class SectorReader : public BlobReader
// Factory function - examines the path to choose the right type of BlobReader, and returns one.
std::unique_ptr<BlobReader> CreateBlobReader(const std::string& filename);

typedef bool (*CompressCB)(const std::string& text, float percent, void* arg);
using CompressCB = std::function<bool(const std::string& text, float percent)>;

bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
const std::string& outfile_path, u32 sub_type, int sector_size = 16384,
CompressCB callback = nullptr, void* arg = nullptr);
const std::string& outfile_path, u32 sub_type, int sector_size,
CompressCB callback);
bool ConvertToPlain(BlobReader* infile, const std::string& infile_path,
const std::string& outfile_path, CompressCB callback = nullptr,
void* arg = nullptr);
const std::string& outfile_path, CompressCB callback);
bool ConvertToWIAOrRVZ(BlobReader* infile, const std::string& infile_path,
const std::string& outfile_path, bool rvz,
WIARVZCompressionType compression_type, int compression_level,
int chunk_size, CompressCB callback = nullptr, void* arg = nullptr);
int chunk_size, CompressCB callback);

} // namespace DiscIO
@@ -238,7 +238,7 @@ static ConversionResult<OutputParameters> Compress(CompressThreadState* state,

static ConversionResultCode Output(OutputParameters parameters, File::IOFile* outfile,
u64* position, std::vector<u64>* offsets, int progress_monitor,
u32 num_blocks, CompressCB callback, void* arg)
u32 num_blocks, CompressCB callback)
{
u64 offset = *position;
if (!parameters.compressed)
@@ -261,7 +261,7 @@ static ConversionResultCode Output(OutputParameters parameters, File::IOFile* ou

const float completion = static_cast<float>(parameters.block_number) / num_blocks;

if (!callback(text, completion, arg))
if (!callback(text, completion))
return ConversionResultCode::Canceled;
}

@@ -270,7 +270,7 @@ static ConversionResultCode Output(OutputParameters parameters, File::IOFile* ou

bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
const std::string& outfile_path, u32 sub_type, int block_size,
CompressCB callback, void* arg)
CompressCB callback)
{
ASSERT(infile->IsDataSizeAccurate());

@@ -284,7 +284,7 @@ bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
return false;
}

callback(Common::GetStringT("Files opened, ready to compress."), 0, arg);
callback(Common::GetStringT("Files opened, ready to compress."), 0);

CompressedBlobHeader header;
header.magic_cookie = GCZ_MAGIC;
@@ -317,7 +317,7 @@ bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,

const auto output = [&](OutputParameters parameters) {
return Output(std::move(parameters), &outfile, &position, &offsets, progress_monitor,
header.num_blocks, callback, arg);
header.num_blocks, callback);
};

MultithreadedCompressor<CompressThreadState, CompressParameters, OutputParameters> compressor(
@@ -364,7 +364,7 @@ bool ConvertToGCZ(BlobReader* infile, const std::string& infile_path,
outfile.WriteArray(offsets.data(), header.num_blocks);
outfile.WriteArray(hashes.data(), header.num_blocks);

callback(Common::GetStringT("Done compressing disc image."), 1.0f, arg);
callback(Common::GetStringT("Done compressing disc image."), 1.0f);
}

if (result == ConversionResultCode::ReadFailed)
@@ -42,7 +42,7 @@ bool PlainFileReader::Read(u64 offset, u64 nbytes, u8* out_ptr)
}

bool ConvertToPlain(BlobReader* infile, const std::string& infile_path,
const std::string& outfile_path, CompressCB callback, void* arg)
const std::string& outfile_path, CompressCB callback)
{
ASSERT(infile->IsDataSizeAccurate());

@@ -78,7 +78,7 @@ bool ConvertToPlain(BlobReader* infile, const std::string& infile_path,
if (i % progress_monitor == 0)
{
const bool was_cancelled =
!callback(Common::GetStringT("Unpacking"), (float)i / (float)num_buffers, arg);
!callback(Common::GetStringT("Unpacking"), (float)i / (float)num_buffers);
if (was_cancelled)
{
success = false;
@@ -1686,9 +1686,9 @@ ConversionResultCode WIARVZFileReader<RVZ>::Output(std::vector<OutputParametersE
}

template <bool RVZ>
ConversionResultCode
WIARVZFileReader<RVZ>::RunCallback(size_t groups_written, u64 bytes_read, u64 bytes_written,
u32 total_groups, u64 iso_size, CompressCB callback, void* arg)
ConversionResultCode WIARVZFileReader<RVZ>::RunCallback(size_t groups_written, u64 bytes_read,
u64 bytes_written, u32 total_groups,
u64 iso_size, CompressCB callback)
{
int ratio = 0;
if (bytes_read != 0)
@@ -1700,8 +1700,8 @@ WIARVZFileReader<RVZ>::RunCallback(size_t groups_written, u64 bytes_read, u64 by

const float completion = static_cast<float>(bytes_read) / iso_size;

return callback(text, completion, arg) ? ConversionResultCode::Success :
ConversionResultCode::Canceled;
return callback(text, completion) ? ConversionResultCode::Success :
ConversionResultCode::Canceled;
}

template <bool RVZ>
@@ -1729,8 +1729,7 @@ template <bool RVZ>
ConversionResultCode
WIARVZFileReader<RVZ>::Convert(BlobReader* infile, const VolumeDisc* infile_volume,
File::IOFile* outfile, WIARVZCompressionType compression_type,
int compression_level, int chunk_size, CompressCB callback,
void* arg)
int compression_level, int chunk_size, CompressCB callback)
{
ASSERT(infile->IsDataSizeAccurate());
ASSERT(chunk_size > 0);
@@ -1832,7 +1831,7 @@ WIARVZFileReader<RVZ>::Convert(BlobReader* infile, const VolumeDisc* infile_volu
return result;

return RunCallback(parameters.group_index + parameters.entries.size(), parameters.bytes_read,
bytes_written, total_groups, iso_size, callback, arg);
bytes_written, total_groups, iso_size, callback);
};

MultithreadedCompressor<CompressThreadState, CompressParameters, OutputParameters> mt_compressor(
@@ -2030,7 +2029,7 @@ WIARVZFileReader<RVZ>::Convert(BlobReader* infile, const VolumeDisc* infile_volu
bool ConvertToWIAOrRVZ(BlobReader* infile, const std::string& infile_path,
const std::string& outfile_path, bool rvz,
WIARVZCompressionType compression_type, int compression_level,
int chunk_size, CompressCB callback, void* arg)
int chunk_size, CompressCB callback)
{
File::IOFile outfile(outfile_path, "wb");
if (!outfile)
@@ -2047,7 +2046,7 @@ bool ConvertToWIAOrRVZ(BlobReader* infile, const std::string& infile_path,
const auto convert = rvz ? RVZFileReader::Convert : WIAFileReader::Convert;
const ConversionResultCode result =
convert(infile, infile_volume.get(), &outfile, compression_type, compression_level,
chunk_size, callback, arg);
chunk_size, callback);

if (result == ConversionResultCode::ReadFailed)
PanicAlertT("Failed to read from the input file \"%s\".", infile_path.c_str());
@@ -64,8 +64,7 @@ class WIARVZFileReader : public BlobReader

static ConversionResultCode Convert(BlobReader* infile, const VolumeDisc* infile_volume,
File::IOFile* outfile, WIARVZCompressionType compression_type,
int compression_level, int chunk_size, CompressCB callback,
void* arg);
int compression_level, int chunk_size, CompressCB callback);

private:
using SHA1 = std::array<u8, 20>;
@@ -351,8 +350,7 @@ class WIARVZFileReader : public BlobReader
std::mutex* reusable_groups_mutex, GroupEntry* group_entry,
u64* bytes_written);
static ConversionResultCode RunCallback(size_t groups_written, u64 bytes_read, u64 bytes_written,
u32 total_groups, u64 iso_size, CompressCB callback,
void* arg);
u32 total_groups, u64 iso_size, CompressCB callback);

bool m_valid;
WIARVZCompressionType m_compression_type;
@@ -32,17 +32,6 @@
#include "UICommon/GameFile.h"
#include "UICommon/UICommon.h"

static bool CompressCB(const std::string& text, float percent, void* ptr)
{
if (ptr == nullptr)
return false;

auto* progress_dialog = static_cast<ParallelProgressDialog*>(ptr);

progress_dialog->SetValue(percent * 100);
return !progress_dialog->WasCanceled();
}

ConvertDialog::ConvertDialog(QList<std::shared_ptr<const UICommon::GameFile>> files,
QWidget* parent)
: QDialog(parent), m_files(std::move(files))
@@ -463,26 +452,29 @@ void ConvertDialog::Convert()
}
else
{
const auto callback = [&progress_dialog](const std::string& text, float percent) {
progress_dialog.SetValue(percent * 100);
return !progress_dialog.WasCanceled();
};

std::future<bool> success;

switch (format)
{
case DiscIO::BlobType::PLAIN:
success = std::async(std::launch::async, [&] {
const bool good =
DiscIO::ConvertToPlain(blob_reader.get(), original_path, dst_path.toStdString(),
&CompressCB, &progress_dialog);
const bool good = DiscIO::ConvertToPlain(blob_reader.get(), original_path,
dst_path.toStdString(), callback);
progress_dialog.Reset();
return good;
});
break;

case DiscIO::BlobType::GCZ:
success = std::async(std::launch::async, [&] {
const bool good =
DiscIO::ConvertToGCZ(blob_reader.get(), original_path, dst_path.toStdString(),
file->GetPlatform() == DiscIO::Platform::WiiDisc ? 1 : 0,
block_size, &CompressCB, &progress_dialog);
const bool good = DiscIO::ConvertToGCZ(
blob_reader.get(), original_path, dst_path.toStdString(),
file->GetPlatform() == DiscIO::Platform::WiiDisc ? 1 : 0, block_size, callback);
progress_dialog.Reset();
return good;
});
@@ -491,10 +483,10 @@ void ConvertDialog::Convert()
case DiscIO::BlobType::WIA:
case DiscIO::BlobType::RVZ:
success = std::async(std::launch::async, [&] {
const bool good = DiscIO::ConvertToWIAOrRVZ(
blob_reader.get(), original_path, dst_path.toStdString(),
format == DiscIO::BlobType::RVZ, compression, compression_level, block_size,
&CompressCB, &progress_dialog);
const bool good =
DiscIO::ConvertToWIAOrRVZ(blob_reader.get(), original_path, dst_path.toStdString(),
format == DiscIO::BlobType::RVZ, compression,
compression_level, block_size, callback);
progress_dialog.Reset();
return good;
});
@@ -267,9 +267,8 @@ void GameList::ShowContextMenu(const QPoint&)
{
const auto selected_games = GetSelectedGames();

if (std::all_of(selected_games.begin(), selected_games.end(), [](const auto& game) {
return DiscIO::IsDisc(game->GetPlatform()) && game->IsVolumeSizeAccurate();
}))
if (std::all_of(selected_games.begin(), selected_games.end(),
[](const auto& game) { return game->ShouldAllowConversion(); }))
{
menu->addAction(tr("Convert Selected Files..."), this, &GameList::ConvertFile);
menu->addSeparator();
@@ -301,7 +300,7 @@ void GameList::ShowContextMenu(const QPoint&)
{
menu->addAction(tr("Set as &Default ISO"), this, &GameList::SetDefaultISO);

if (game->IsVolumeSizeAccurate())
if (game->ShouldAllowConversion())
menu->addAction(tr("Convert File..."), this, &GameList::ConvertFile);

QAction* change_disc = menu->addAction(tr("Change &Disc"), this, &GameList::ChangeDisc);
@@ -672,6 +672,11 @@ std::string GameFile::GetFileFormatName() const
}
}

bool GameFile::ShouldAllowConversion() const
{
return DiscIO::IsDisc(m_platform) && m_volume_size_is_accurate;
}

const GameBanner& GameFile::GetBannerImage() const
{
return m_custom_banner.empty() ? m_volume_banner : m_custom_banner;
@@ -101,6 +101,7 @@ class GameFile final
const std::string& GetCompressionMethod() const { return m_compression_method; }
bool ShouldShowFileFormatDetails() const;
std::string GetFileFormatName() const;
bool ShouldAllowConversion() const;
const std::string& GetApploaderDate() const { return m_apploader_date; }
u64 GetFileSize() const { return m_file_size; }
u64 GetVolumeSize() const { return m_volume_size; }