Merge pull request #320 from AngelAuraMC/fix/modpack-import-perf-issue

Fix/modpack import perf issue
This commit is contained in:
tomikun
2026-07-29 21:55:00 +08:00
committed by GitHub
8 changed files with 168 additions and 158 deletions

View File

@@ -73,7 +73,7 @@ public class ProgressLayout extends ConstraintLayout implements View.OnClickList
}
}
public boolean hasProcesses(){
public static boolean hasProcesses(){
return ProgressKeeper.getTaskCount() > 0;
}

View File

@@ -10,8 +10,10 @@ import android.Manifest;
import android.app.NotificationManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Build;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.util.Log;
import android.view.View;
import android.widget.ImageButton;
@@ -60,11 +62,14 @@ import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.List;
import java.util.Locale;
public class LauncherActivity extends BaseActivity {
public static final String SETTING_FRAGMENT_TAG = "SETTINGS_FRAGMENT";
@@ -78,14 +83,42 @@ public class LauncherActivity extends BaseActivity {
if(data != null) {
PojavApplication.sExecutorService.execute(() -> {
try {
ModLoader loaderInfo = new CommonApi(getString(R.string.curseforge_api_key)).importModpack(this, data);
// Copy ZIP file to cache
long fileSize = -1;
try (Cursor returnCursor = getContentResolver().query(data, new String[]{OpenableColumns.SIZE}, null, null, null)) {
if (returnCursor != null && returnCursor.moveToFirst()) {
fileSize = returnCursor.getLong(0);
}
}
File modpackFile = new File(Tools.DIR_CACHE, "import_modpack_placeholdername.cf");
try (InputStream inputStream = getContentResolver().openInputStream(data)){
FileOutputStream output = new FileOutputStream(modpackFile);
byte[] b = new byte[262144];
int read;
int readTotal = 0;
while ((read = inputStream.read(b)) != -1) {
output.write(b, 0, read);
readTotal += read;
String readMB = fileSize > 0 ? String.format(Locale.US, "%.2f", readTotal / (1024.0 * 1024.0)) : "unknown";
String totalMB = fileSize > 0 ? String.format(Locale.US, "%.2f", fileSize / (1024.0 * 1024.0)) : "unknown";
int progress = fileSize > 0 ? (int) ((readTotal * 100L) / fileSize) : 0;
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, progress, R.string.import_modpack_copy, readMB, totalMB);
}
output.flush();
output.close();
}
ModLoader loaderInfo = new CommonApi(getString(R.string.curseforge_api_key)).importModpack(modpackFile);
modpackFile.delete();
if (loaderInfo == null) return;
loaderInfo.getDownloadTask(new NotificationDownloadListener(this, loaderInfo)).run();
} catch (IOException e) {
Tools.showErrorRemote(this, R.string.modpack_install_download_failed, e);
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
} catch (IllegalArgumentException e) {
Tools.showError(this, R.string.not_modpack_file, e);
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
} catch (NoSuchAlgorithmException e) {
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
// Should literally never happen because SHA-1 is required Java spec
throw new RuntimeException(e);
}
@@ -138,7 +171,7 @@ public class LauncherActivity extends BaseActivity {
};
private final ExtraListener<Boolean> mLaunchGameListener = (key, value) -> {
if(mProgressLayout.hasProcesses()){
if(ProgressLayout.hasProcesses()){
Toast.makeText(this, R.string.tasks_ongoing, Toast.LENGTH_LONG).show();
return false;
}

View File

@@ -6,6 +6,7 @@ import static net.kdt.pojavlaunch.Tools.hasOnlineProfile;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
@@ -13,6 +14,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.kdt.mcgui.MineButton;
import com.kdt.mcgui.ProgressLayout;
import net.kdt.pojavlaunch.BaseActivity;
import net.kdt.pojavlaunch.LauncherActivity;
@@ -36,6 +38,10 @@ public class ModpackCreateFragment extends Fragment {
Activity launcheractivity = requireActivity();
if (!(launcheractivity instanceof LauncherActivity))
throw new IllegalStateException("Cannot import modpack without LauncherActivity");
if(ProgressLayout.hasProcesses()){
Toast.makeText(launcheractivity, R.string.tasks_ongoing, Toast.LENGTH_LONG).show();
return;
}
((LauncherActivity) launcheractivity).modpackImportLauncher.launch(null);
});;
}

View File

@@ -14,6 +14,7 @@ import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult;
import net.kdt.pojavlaunch.utils.ZipUtils;
import org.jdom2.IllegalDataException;
@@ -26,6 +27,7 @@ import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
/**
@@ -124,8 +126,8 @@ public class CommonApi implements ModpackApi {
}
@Override
public ModLoader importModpack(Activity activity, Uri zipUri) throws IOException, NoSuchAlgorithmException {
return getModpackApi(activity, zipUri).importModpack(activity, zipUri);
public ModLoader importModpack(File modpackFile) throws IOException, NoSuchAlgorithmException {
return getModpackApi(modpackFile).importModpack(modpackFile);
}
private @NonNull ModpackApi getModpackApi(int apiSource) {
@@ -139,29 +141,37 @@ public class CommonApi implements ModpackApi {
}
}
private @NonNull ModpackApi getModpackApi(Activity activity, Uri zipUri){
String modrinthPackInfoFileName = "modrinth.index.json";
String curseforgePackInfoFileName = "manifest.json";
InputStream inputStream = null;
try {
inputStream = activity.getContentResolver().openInputStream(zipUri);
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
ZipEntry zipEntry;
private @NonNull ModpackApi getModpackApi(@NonNull File modpackFile) throws IOException {
try (ZipFile zip = new ZipFile(modpackFile)) {
boolean isModrinth;
boolean isCurseforge;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
isModrinth = zipEntry.getName().equals(modrinthPackInfoFileName);
isCurseforge = zipEntry.getName().equals(curseforgePackInfoFileName);
if(isModrinth) {
return mModrinthApi;
} else if (isCurseforge) {
return mCurseforgeApi;
}
try {
ZipUtils.getEntryStream(zip, "modrinth.index.json");
isModrinth = true;
} catch (IOException ignored) {
isModrinth = false;
}
} catch (Exception e) {
throw new RuntimeException(e);
try {
ZipUtils.getEntryStream(zip, "manifest.json");
isCurseforge = true;
} catch (IOException ignored) {
isCurseforge = false;
}
if (isModrinth && isCurseforge) {
String name = modpackFile.getName();
int dot = name.lastIndexOf('.');
String extension = (dot == -1) ? "" : name.substring(dot + 1);
if (extension.equalsIgnoreCase("mrpack")) return mModrinthApi;
throw new IOException("Ambiguous file contains both modrinth.index.json and manifest.json. Cannot determine modpack format.");
}
if (isModrinth) return mModrinthApi;
if (isCurseforge) return mCurseforgeApi;
throw new IllegalArgumentException("Zip provided does not contain a manifest file.");
}
throw new IllegalArgumentException("Zip provided does not contain a manifest file");
}
/** Fuse the arrays in a way that's fair for every endpoint */

View File

@@ -176,8 +176,8 @@ public class CurseforgeApi implements ModpackApi{
}
@Override
public ModLoader importModpack(Activity activity, Uri zipUri) throws IOException, NoSuchAlgorithmException {
return ModpackInstaller.importModpack(activity, zipUri, this::installCurseforgeZip);
public ModLoader importModpack(File modpackFile) throws IOException, NoSuchAlgorithmException {
return ModpackInstaller.importModpack(modpackFile, Constants.SOURCE_CURSEFORGE, this::installCurseforgeZip);
}

View File

@@ -81,8 +81,7 @@ public interface ModpackApi {
* Imports the mod(pack) from a file.
* May require the download of additional files.
* May requires launching the installation of a modloader
* @param activity any activity
* @param zipUri URI to DocumentsUI selected zip file
* @param modpackFile Zip file to mrpack or cf zip pack
*/
ModLoader importModpack(Activity activity, Uri zipUri) throws IOException, NoSuchAlgorithmException;
ModLoader importModpack(File modpackFile) throws IOException, NoSuchAlgorithmException;
}

View File

@@ -1,10 +1,5 @@
package net.kdt.pojavlaunch.modloaders.modpacks.api;
import android.app.Activity;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.kdt.mcgui.ProgressLayout;
@@ -12,26 +7,23 @@ import com.kdt.mcgui.ProgressLayout;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ModIconCache;
import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper;
import net.kdt.pojavlaunch.utils.DownloadUtils;
import net.kdt.pojavlaunch.utils.ZipUtils;
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipFile;
public class ModpackInstaller {
@@ -65,6 +57,7 @@ public class ModpackInstaller {
modLoaderInfo = installFunction.installModpack(modpackFile, new File(Tools.DIR_GAME_HOME, "custom_instances/"+modpackName));
} finally {
//noinspection ResultOfMethodCallIgnored It's cache, who cares
modpackFile.delete();
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
}
@@ -86,123 +79,92 @@ public class ModpackInstaller {
return modLoaderInfo;
}
public static ModLoader importModpack(Activity activity, Uri zipUri, InstallFunction installFunction) throws IOException, NoSuchAlgorithmException {
public static ModLoader importModpack(File modpackFile, int apiSource, InstallFunction installFunction) throws IOException, NoSuchAlgorithmException {
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 1, R.string.import_modpack_start);
String modrinthPackInfoFileName = "modrinth.index.json";
String curseforgePackInfoFileName = "manifest.json";
InputStream inputStream = activity.getContentResolver().openInputStream(zipUri);
if (inputStream == null) throw new IOException("Can't open modpack file, try again?");
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
boolean isModrinth = zipEntry.getName().equals(modrinthPackInfoFileName);
boolean isCurseforge = zipEntry.getName().equals(curseforgePackInfoFileName);
if (!(isModrinth || isCurseforge)) continue;
// Read Manifest JSON
BufferedReader reader = new BufferedReader(new InputStreamReader(zipInputStream));
String str;
StringBuilder jsonString = new StringBuilder();
while ((str = reader.readLine()) != null) {
jsonString.append(str).append("\n");
}
zipInputStream.close();
// Hash the ZIP File
inputStream = activity.getContentResolver().openInputStream(zipUri);
if (inputStream == null) throw new IOException("Can't open modpack file, try again?");
MessageDigest algorithm = MessageDigest.getInstance("SHA-1");
DigestInputStream hashingStream = new DigestInputStream(inputStream, algorithm);
long fileSize = -1;
long readSize = 0;
try (Cursor returnCursor = activity.getContentResolver().query(zipUri, new String[]{OpenableColumns.SIZE}, null, null, null)) {
if (returnCursor != null && returnCursor.moveToFirst()) {
fileSize = returnCursor.getLong(0);
}
}
byte[] buffer = new byte[262144];
while (true) {
int n = hashingStream.read(buffer);
if (n == -1) break;
readSize += n;
String readMB = fileSize > 0 ? String.format(Locale.US, "%.2f", readSize / (1024.0 * 1024.0)) : "unknown";
String totalMB = fileSize > 0 ? String.format(Locale.US, "%.2f",fileSize / (1024.0 * 1024.0)) : "unknown";
int progress = fileSize > 0 ? (int) ((readSize * 100L) / fileSize) : 0;
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, progress, R.string.import_modpack_hash, readMB, totalMB);
}
hashingStream.close();
byte[] digest = algorithm.digest();
StringBuilder sb = new StringBuilder(digest.length * 2);
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
String hash = sb.toString();
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.import_modpack_json);
// Parse the JSON to prepare for instance creation
JsonObject packInfoJson = JsonParser.parseString(jsonString.toString()).getAsJsonObject();
String modpackName = "";
String modpackVersion = "";
String modpackMcVersion = "";
if(isModrinth){
try {
modpackName = packInfoJson.get("name").getAsString();
modpackVersion = packInfoJson.get("versionId").getAsString();
modpackMcVersion = packInfoJson.get("dependencies").getAsJsonObject().get("minecraft").getAsString();
} catch (RuntimeException ignored) {}
} else {
try {
modpackName = packInfoJson.get("name").getAsString();
modpackVersion = packInfoJson.get("version").getAsString();
modpackMcVersion = packInfoJson.get("minecraft").getAsJsonObject().get("version").getAsString();
} catch (RuntimeException ignored) {}
}
if(modpackName.isBlank() || modpackVersion.isBlank() || modpackMcVersion.isBlank()) throw new IOException("Corrupt Modpack manifest file.");
// Added a for because there is an awkward __ that I can't be bothered to fix
// FO only deduplication be like:
String profileFolderName = String.join(" ", modpackName, modpackVersion, "for", modpackMcVersion, hash);
profileFolderName = profileFolderName.trim().replaceAll("[\\\\/:*?\"<>| \\t\\n]", "_");
// Copy ZIP file to cache
File modpackFile = new File(Tools.DIR_CACHE, profileFolderName + ".cf");
inputStream = activity.getContentResolver().openInputStream(zipUri);
if (inputStream == null) throw new IOException("Can't open modpack file, try again?");
FileOutputStream output = new FileOutputStream(modpackFile);
byte[] b = new byte[262144];
int read;
int readTotal = 0;
while ((read = inputStream.read(b)) != -1) {
output.write(b, 0, read);
readTotal += read;
String readMB = fileSize > 0 ? String.format(Locale.US, "%.2f", readTotal / (1024.0 * 1024.0)) : "unknown";
String totalMB = fileSize > 0 ? String.format(Locale.US, "%.2f", fileSize / (1024.0 * 1024.0)) : "unknown";
int progress = fileSize > 0 ? (int) ((readTotal * 100L) / fileSize) : 0;
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, progress, R.string.import_modpack_copy, readMB, totalMB);
}
output.flush();
output.close();
// Install the actual pack into custom_instances
ModLoader modLoaderInfo = installFunction.installModpack(modpackFile, new File(Tools.DIR_GAME_HOME, "custom_instances/"+profileFolderName));
// We have to do this because installModpack doesn't clean up after itself
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
modpackFile.delete();
// Create the instance (We don't have a picture guys)
MinecraftProfile profile = MinecraftProfile.getDefaultProfile();
profile.gameDir = "./custom_instances/" + profileFolderName;
profile.name = modpackName;
if (!modpackMcVersion.isBlank()) profile.lastVersionId = modpackMcVersion;
if (modLoaderInfo != null && modLoaderInfo.getVersionId() != null)
profile.lastVersionId = modLoaderInfo.getVersionId();
LauncherProfiles.mainProfileJson.profiles.put(profileFolderName, profile);
LauncherProfiles.write();
return modLoaderInfo;
// modpackFile is deleted in LauncherActivity, no need to delete here.
if (modpackFile == null) throw new IOException("Can't open modpack file, try again?");
String manifestFileName;
switch (apiSource) {
case Constants.SOURCE_CURSEFORGE:
manifestFileName = "manifest.json";
break;
case Constants.SOURCE_MODRINTH:
manifestFileName = "modrinth.index.json";
break;
default:
throw new UnsupportedOperationException("Unknown API source: " + apiSource);
}
throw new IOException("Can't find manifest file in modpack provided");
// Read Manifest JSON
JsonObject manifestFile = JsonParser.parseString(Tools.read(ZipUtils.getEntryStream(
new ZipFile(modpackFile), manifestFileName))).getAsJsonObject();
// Parse the JSON to prepare for instance creation
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 1, R.string.import_modpack_json);
String modpackName = "";
String modpackVersion = "";
String modpackMcVersion = "";
switch (apiSource) {
case Constants.SOURCE_CURSEFORGE:
try {
modpackName = manifestFile.get("name").getAsString();
modpackVersion = manifestFile.get("version").getAsString();
modpackMcVersion = manifestFile.get("minecraft").getAsJsonObject().get("version").getAsString();
} catch (RuntimeException ignored) {}
break;
case Constants.SOURCE_MODRINTH:
try {
modpackName = manifestFile.get("name").getAsString();
modpackVersion = manifestFile.get("versionId").getAsString();
modpackMcVersion = manifestFile.get("dependencies").getAsJsonObject().get("minecraft").getAsString();
} catch (RuntimeException ignored) {}
break;
default:
throw new UnsupportedOperationException("Unknown API source: " + apiSource);
}
if(modpackName.isBlank() || modpackVersion.isBlank() || modpackMcVersion.isBlank()) throw new IOException("Corrupt Modpack manifest file.");
// Hash the ZIP File, can't use getSha1 cause progress bar
MessageDigest algorithm = MessageDigest.getInstance("SHA-1");
//noinspection IOStreamConstructor It will reccomend you use an API26 function like a dumb
DigestInputStream hashingStream = new DigestInputStream(new FileInputStream(modpackFile), algorithm);
long fileSize = modpackFile.length();
long readSize = 0;
byte[] buffer = new byte[262144];
while (true) {
int n = hashingStream.read(buffer);
if (n == -1) break;
readSize += n;
String readMB = fileSize > 0 ? String.format(Locale.US, "%.2f", readSize / (1024.0 * 1024.0)) : "unknown";
String totalMB = fileSize > 0 ? String.format(Locale.US, "%.2f",fileSize / (1024.0 * 1024.0)) : "unknown";
int progress = fileSize > 0 ? (int) ((readSize * 100L) / fileSize) : 0;
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, progress, R.string.import_modpack_hash, readMB, totalMB);
}
hashingStream.close();
byte[] digest = algorithm.digest();
StringBuilder sb = new StringBuilder(digest.length * 2);
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
String hash = sb.toString();
String profileFolderName = String.join(" ", modpackName, modpackVersion, "for", modpackMcVersion, hash);
profileFolderName = profileFolderName.trim().replaceAll("[\\\\/:*?\"<>| \\t\\n]", "_");
// Install the actual pack into custom_instances
ModLoader modLoaderInfo = installFunction.installModpack(modpackFile, new File(Tools.DIR_GAME_HOME, "custom_instances/"+profileFolderName));
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
// Create the instance (We don't have a picture guys)
MinecraftProfile profile = MinecraftProfile.getDefaultProfile();
profile.gameDir = "./custom_instances/" + profileFolderName;
profile.name = modpackName;
if (!modpackMcVersion.isBlank()) profile.lastVersionId = modpackMcVersion;
if (modLoaderInfo != null && modLoaderInfo.getVersionId() != null)
profile.lastVersionId = modLoaderInfo.getVersionId();
LauncherProfiles.mainProfileJson.profiles.put(profileFolderName, profile);
LauncherProfiles.write();
return modLoaderInfo;
}
interface InstallFunction {

View File

@@ -160,8 +160,8 @@ public class ModrinthApi implements ModpackApi{
}
@Override
public ModLoader importModpack(Activity activity, Uri zipUri) throws IOException, NoSuchAlgorithmException {
return ModpackInstaller.importModpack(activity, zipUri, this::installMrpack);
public ModLoader importModpack(File modpackFile) throws IOException, NoSuchAlgorithmException {
return ModpackInstaller.importModpack(modpackFile, Constants.SOURCE_MODRINTH, this::installMrpack);
}
private static ModLoader createInfo(ModrinthIndex modrinthIndex) {