Major changes

- Added a textview for no content/network error messages
- Reimplemented Modrinth modpack installer with parallel installation in mind
- Implemented modloader version ID generation
- Implemented profile icons

TODO:
- Actually figure out how to install the mod loader
- Proper IOException handling in the modpack installer
- Proper IOException handling when loading detailed mod info
- CurseForge
This commit is contained in:
BuildTools
2023-08-08 14:01:52 +03:00
committed by ArtDev
parent 586fef897e
commit 5fde03dbaa
17 changed files with 410 additions and 87 deletions

View File

@@ -79,6 +79,7 @@ import java.util.Map;
@SuppressWarnings("IOStreamConstructor")
public final class Tools {
public static final float BYTE_TO_MB = 1024 * 1024;
public static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
public static String APP_NAME = "null";

View File

@@ -1,5 +1,7 @@
package net.kdt.pojavlaunch.fragments;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
@@ -37,6 +39,8 @@ public class SearchModFragment extends Fragment {
private ModItemAdapter mModItemAdapter;
private ProgressBar mSearchProgressBar;
private Future<?> mSearchFuture;
private TextView mStatusTextView;
private ColorStateList mDefaultTextColor;
private ModpackApi modpackApi;
@@ -59,6 +63,9 @@ public class SearchModFragment extends Fragment {
mSelectedVersion = view.findViewById(R.id.search_mod_selected_mc_version_textview);
mSelectVersionButton = view.findViewById(R.id.search_mod_mc_version_button);
mRecyclerview = view.findViewById(R.id.search_mod_list);
mStatusTextView = view.findViewById(R.id.search_mod_status_text);
mDefaultTextColor = mStatusTextView.getTextColors();
mRecyclerview.setLayoutManager(new LinearLayoutManager(getContext()));
mRecyclerview.setAdapter(mModItemAdapter);
@@ -81,6 +88,7 @@ public class SearchModFragment extends Fragment {
return true;
});
}
class SearchModRunnable implements Runnable{
private final Object mFutureLock = new Object();
private final SearchFilters mRunnableFilters;
@@ -109,9 +117,22 @@ public class SearchModFragment extends Fragment {
ModItem[] items = modpackApi.searchMod(mRunnableFilters);
Log.d(SearchModFragment.class.toString(), Arrays.toString(items));
Tools.runOnUiThread(() -> {
ModItem[] localItems = items;
if(mMyFuture.isCancelled()) return;
mSearchProgressBar.setVisibility(View.GONE);
mModItemAdapter.setModItems(items, mSelectedVersion.getText().toString());
if(localItems == null) {
mStatusTextView.setVisibility(View.VISIBLE);
mStatusTextView.setTextColor(Color.RED);
mStatusTextView.setText(R.string.search_modpack_error);
}else if(localItems.length == 0) {
mStatusTextView.setVisibility(View.VISIBLE);
mStatusTextView.setTextColor(mDefaultTextColor);
mStatusTextView.setText(R.string.search_modpack_no_result);
localItems = null;
}else{
mStatusTextView.setVisibility(View.GONE);
}
mModItemAdapter.setModItems(localItems, mSelectedVersion.getText().toString());
});
}
}

View File

@@ -1,5 +1,6 @@
package net.kdt.pojavlaunch.modloaders.modpacks;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.view.LayoutInflater;
import android.view.View;
@@ -9,7 +10,6 @@ import android.widget.Button;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@@ -28,7 +28,7 @@ import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
import java.util.Arrays;
public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHolder> {
private static final ModItem[] MOD_ITEMS_EMPTY = new ModItem[0];
private final ModIconCache mIconCache = new ModIconCache();
private ModItem[] mModItems;
private final ModpackApi mModpackApi;
@@ -44,7 +44,7 @@ public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHold
private View mExtendedLayout;
private Spinner mExtendedSpinner;
private Button mExtendedButton;
private ImageView mIconView;
private final ImageView mIconView;
private Bitmap mThumbnailBitmap;
private ImageReceiver mImageReceiver;
public ViewHolder(View view) {
@@ -102,7 +102,7 @@ public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHold
mThumbnailBitmap = bm;
mIconView.setImageBitmap(bm);
};
mIconCache.getImage(mImageReceiver, mModItem.apiSource+"_"+mModItem.id, mModItem.imageUrl);
mIconCache.getImage(mImageReceiver, mModItem.getIconCacheTag(), mModItem.imageUrl);
mTitle.setText(item.title);
mDescription.setText(item.description);
@@ -132,8 +132,11 @@ public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHold
mModItems = new ModItem[]{};
}
@SuppressLint("NotifyDataSetChanged")
public void setModItems(ModItem[] items, String targetMcVersion){
mModItems = items;
// TODO: Use targetMcVersion to affect default selected modpack version
if(items != null) mModItems = items;
else mModItems = MOD_ITEMS_EMPTY;
notifyDataSetChanged();
}

View File

@@ -3,17 +3,16 @@ package net.kdt.pojavlaunch.modloaders.modpacks.api;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import net.kdt.pojavlaunch.Tools;
import java.io.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.stream.Collectors;
public class ApiHandler {
public final String baseUrl;

View File

@@ -0,0 +1,117 @@
package net.kdt.pojavlaunch.modloaders.modpacks.api;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.utils.DownloadUtils;
import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
public class ModDownloader {
private static final ThreadLocal<byte[]> sThreadLocalBuffer = new ThreadLocal<>();
private final ThreadPoolExecutor mDownloadPool = new ThreadPoolExecutor(4,4,100, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
private final AtomicBoolean mTerminator = new AtomicBoolean(false);
private final AtomicLong mDownloadSize = new AtomicLong(0);
private final Object mExceptionSyncPoint = new Object();
private final File mDestinationDirectory;
private IOException mFirstIOException;
private long mTotalSize;
public ModDownloader(File destinationDirectory) {
this.mDestinationDirectory = destinationDirectory;
}
public void submitDownload(int fileSize, String relativePath, String... url) {
mTotalSize += fileSize;
mDownloadPool.execute(new DownloadTask(url, new File(mDestinationDirectory, relativePath)));
}
public void awaitFinish(Tools.DownloaderFeedback feedback) throws IOException{
try {
mDownloadPool.shutdown();
while(!mDownloadPool.awaitTermination(20, TimeUnit.MILLISECONDS) && !mTerminator.get()) {
feedback.updateProgress((int) mDownloadSize.get(), (int) mTotalSize);
}
if(mTerminator.get()) {
synchronized (mExceptionSyncPoint) {
if(mFirstIOException == null) mExceptionSyncPoint.wait();
throw mFirstIOException;
}
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}
private static byte[] getThreadLocalBuffer() {
byte[] buffer = sThreadLocalBuffer.get();
if(buffer != null) return buffer;
buffer = new byte[8192];
sThreadLocalBuffer.set(buffer);
return buffer;
}
class DownloadTask implements Runnable, Tools.DownloaderFeedback {
private final String[] mDownloadUrls;
private final File mDestination;
private int last = 0;
public DownloadTask(String[] downloadurls,
File downloadDestination) {
this.mDownloadUrls = downloadurls;
this.mDestination = downloadDestination;
}
@Override
public void run() {
IOException exception = null;
for(String sourceUrl : mDownloadUrls) {
try {
exception = tryDownload(sourceUrl);
if(exception == null) return;
}catch (InterruptedException e) {
return;
}
}
if(exception != null) {
synchronized (mExceptionSyncPoint) {
if(mFirstIOException == null) {
mFirstIOException = exception;
mExceptionSyncPoint.notify();
}
}
}
}
private IOException tryDownload(String sourceUrl) throws InterruptedException {
IOException exception = null;
for (int i = 0; i < 5; i++) {
try {
DownloadUtils.downloadFileMonitored(sourceUrl, mDestination, getThreadLocalBuffer(), this);
return null;
} catch (InterruptedIOException e) {
throw new InterruptedException();
} catch (IOException e) {
e.printStackTrace();
exception = e;
}
mDownloadSize.addAndGet(-last);
last = 0;
}
return exception;
}
@Override
public void updateProgress(int curr, int max) {
mDownloadSize.addAndGet(curr - last);
last = curr;
}
}
}

View File

@@ -0,0 +1,29 @@
package net.kdt.pojavlaunch.modloaders.modpacks.api;
public class ModLoaderInfo {
public static final int MOD_LOADER_FORGE = 0;
public static final int MOD_LOADER_FABRIC = 1;
public static final int MOD_LOADER_QUILT = 2;
public final int modLoaderType;
public final String modLoaderVersion;
public final String minecraftVersion;
public ModLoaderInfo(int modLoaderType, String modLoaderVersion, String minecraftVersion) {
this.modLoaderType = modLoaderType;
this.modLoaderVersion = modLoaderVersion;
this.minecraftVersion = minecraftVersion;
}
public String getVersionId() {
switch (modLoaderType) {
case MOD_LOADER_FORGE:
return minecraftVersion+"-forge-"+modLoaderType;
case MOD_LOADER_FABRIC:
return "fabric-loader-"+modLoaderVersion+"-"+minecraftVersion;
case MOD_LOADER_QUILT:
// TODO
default:
return null;
}
}
}

View File

@@ -5,9 +5,6 @@ import net.kdt.pojavlaunch.PojavApplication;
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.progresskeeper.ProgressKeeper;
import org.jetbrains.annotations.Nullable;
/**
*

View File

@@ -1,18 +1,20 @@
package net.kdt.pojavlaunch.modloaders.modpacks.api;
import android.util.Log;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
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.modloaders.modpacks.models.ModItem;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModrinthIndex;
import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper;
import net.kdt.pojavlaunch.utils.DownloadUtils;
import net.kdt.pojavlaunch.utils.FileUtils;
import net.kdt.pojavlaunch.utils.ZipUtils;
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
@@ -20,9 +22,11 @@ import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.zip.ZipFile;
public class ModrinthApi implements ModpackApi {
private ApiHandler mApiHandler;
public class ModrinthApi implements ModpackApi{
private final ApiHandler mApiHandler;
public ModrinthApi(){
mApiHandler = new ApiHandler("https://api.modrinth.com/v2");
}
@@ -68,7 +72,7 @@ public class ModrinthApi implements ModpackApi {
JsonArray response = mApiHandler.get(String.format("project/%s/version", item.id), JsonArray.class);
if(response == null) return null;
System.out.println(response.toString());
System.out.println(response);
String[] names = new String[response.size()];
String[] mcNames = new String[response.size()];
String[] urls = new String[response.size()];
@@ -87,70 +91,80 @@ public class ModrinthApi implements ModpackApi {
}
@Override
public void installMod(ModDetail modDetail, int selectedVersion){
public void installMod(ModDetail modDetail, int selectedVersion) {
//TODO considering only modpacks for now
String versionUrl = modDetail.versionUrls[selectedVersion];
String modpackName = modDetail.title.toLowerCase(Locale.ROOT).trim().replace(" ", "_" );
// Build a new minecraft instance, folder first
File instanceFolder = new File(Tools.DIR_CACHE, modpackName);
instanceFolder.mkdirs();
// Get the mrpack
File modpackFile = new File(Tools.DIR_CACHE, modpackName + ".mrpack");
ModLoaderInfo modLoaderInfo;
try {
DownloadUtils.downloadFile(versionUrl, modpackFile);
FileUtils.uncompressZip(modpackFile, instanceFolder);
// Get the index
ModrinthIndex index = Tools.GLOBAL_GSON.fromJson(Tools.read(instanceFolder.getAbsolutePath() + "/modrinth.index.json"), ModrinthIndex.class);
System.out.println(index);
// Download mods
for (ModrinthIndex.ModrinthIndexFile file : index.files){
File destFile = new File(instanceFolder, file.path);
destFile.getParentFile().mkdirs();
DownloadUtils.downloadFile(file.downloads[0], destFile);
}
// Apply the overrides
for(String overrideName : new String[]{"overrides", "client-overrides"}) {
File overrideFolder = new File(instanceFolder, overrideName);
if(!overrideFolder.exists() || !overrideFolder.isDirectory()){
continue;
}
for(File file : overrideFolder.listFiles()){
// TODO what if overrides + client-overrides have collisions ?
org.apache.commons.io.FileUtils.moveToDirectory(file, instanceFolder, true);
}
overrideFolder.delete();
}
// Remove server override as it is pointless
org.apache.commons.io.FileUtils.deleteDirectory(new File(instanceFolder, "server-overrides"));
// Move the instance folder
org.apache.commons.io.FileUtils.moveDirectoryToDirectory(instanceFolder, new File(Tools.DIR_GAME_HOME, "custom_instances"), true);
byte[] downloadBuffer = new byte[8192];
DownloadUtils.downloadFileMonitored(versionUrl, modpackFile, downloadBuffer,
new DownloaderProgressWrapper(R.string.modpack_download_downloading_metadata,
ProgressLayout.INSTALL_MODPACK));
ModrinthIndex modrinthIndex = installMrpack(modpackFile, new File(Tools.DIR_GAME_HOME, "custom_instances/"+modpackName));
modLoaderInfo = createInfo(modrinthIndex);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
modpackFile.delete();
try {
org.apache.commons.io.FileUtils.deleteDirectory(instanceFolder);
} catch (IOException e) {
Log.e(ModrinthApi.class.toString(), "Failed to cleanup cache instance folder, if any");
}
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
}
if(modLoaderInfo == null) {
return;
}
// Create the instance
MinecraftProfile profile = new MinecraftProfile();
profile.gameDir = "./custom_instances/" + modpackName;
profile.name = modpackName;
//FIXME add the proper version !
profile.lastVersionId = "1.7.10";
profile.name = modDetail.title;
profile.lastVersionId = modLoaderInfo.getVersionId();
profile.icon = ModIconCache.getBase64Image(modDetail.getIconCacheTag());
LauncherProfiles.mainProfileJson.profiles.put(modpackName, profile);
LauncherProfiles.update();
}
private static ModLoaderInfo createInfo(ModrinthIndex modrinthIndex) {
if(modrinthIndex == null) return null;
Map<String, String> dependencies = modrinthIndex.dependencies;
String mcVersion = dependencies.get("minecraft");
if(mcVersion == null) return null;
String modLoaderVersion;
if((modLoaderVersion = dependencies.get("forge")) != null) {
return new ModLoaderInfo(ModLoaderInfo.MOD_LOADER_FORGE, modLoaderVersion, mcVersion);
}
if((modLoaderVersion = dependencies.get("fabric-loader")) != null) {
return new ModLoaderInfo(ModLoaderInfo.MOD_LOADER_FABRIC, modLoaderVersion, mcVersion);
}
if((modLoaderVersion = dependencies.get("quilt-loader")) != null) {
throw new RuntimeException("Quilt is gay af");
//return new ModLoaderInfo(ModLoaderInfo.MOD_LOADER_QUILT, modLoaderVersion, mcVersion);
}
return null;
}
private ModrinthIndex installMrpack(File mrpackFile, File instanceDestination) throws IOException {
try (ZipFile modpackZipFile = new ZipFile(mrpackFile)){
ModrinthIndex modrinthIndex = Tools.GLOBAL_GSON.fromJson(
Tools.read(ZipUtils.getEntryStream(modpackZipFile, "modrinth.index.json")),
ModrinthIndex.class);
ModDownloader modDownloader = new ModDownloader(instanceDestination);
for(ModrinthIndex.ModrinthIndexFile indexFile : modrinthIndex.files) {
modDownloader.submitDownload(indexFile.fileSize, indexFile.path, indexFile.downloads);
}
modDownloader.awaitFinish(new DownloaderProgressWrapper(R.string.modpack_download_downloading_mods, ProgressLayout.INSTALL_MODPACK));
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.modpack_download_applying_overrides, 1, 2);
ZipUtils.zipExtract(modpackZipFile, "overrides/", instanceDestination);
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 50, R.string.modpack_download_applying_overrides, 2, 2);
ZipUtils.zipExtract(modpackZipFile, "client-overrides/", instanceDestination);
return modrinthIndex;
}
}
}

View File

@@ -1,10 +1,15 @@
package net.kdt.pojavlaunch.modloaders.modpacks.imagecache;
import android.util.Base64;
import android.util.Log;
import net.kdt.pojavlaunch.Tools;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
@@ -70,4 +75,32 @@ public class ModIconCache {
if(isCanceled) Log.i("IconCache", "checkCancelled("+imageReceiver.hashCode()+") == true");
return isCanceled;
}
/**
* Get the base64-encoded version of a cached icon by its tag.
* Note: this functions performs I/O operations, and should not be called on the UI
* thread.
* @param imageTag the icon tag
* @return the base64 encoded image or null if not cached
*/
public static String getBase64Image(String imageTag) {
File imagePath = new File(Tools.DIR_CACHE, "mod_icons/"+imageTag+".ca");
Log.i("IconCache", "Creating base64 version of icon "+imageTag);
if(!imagePath.canRead() || !imagePath.isFile()) {
Log.i("IconCache", "Icon does not exist");
return null;
}
try {
try(FileInputStream fileInputStream = new FileInputStream(imagePath)) {
byte[] imageBytes = IOUtils.toByteArray(fileInputStream);
// reencode to png? who cares! our profile icon cache is an omnivore!
// if some other launcher parses this and dies it is not our problem :troll:
return "data:image/png;base64,"+ Base64.encodeToString(imageBytes, Base64.DEFAULT);
}
}catch (IOException e) {
e.printStackTrace();
return null;
}
}
}

View File

@@ -1,6 +1,8 @@
package net.kdt.pojavlaunch.modloaders.modpacks.models;
import androidx.annotation.NonNull;
import java.util.Arrays;
public class ModDetail extends ModItem {
@@ -15,6 +17,7 @@ public class ModDetail extends ModItem {
this.versionUrls = versionUrls;
}
@NonNull
@Override
public String toString() {
return "ModDetail{" +

View File

@@ -1,5 +1,7 @@
package net.kdt.pojavlaunch.modloaders.modpacks.models;
import androidx.annotation.NonNull;
public class ModItem extends ModSource {
public String id;
@@ -16,6 +18,7 @@ public class ModItem extends ModSource {
this.imageUrl = imageUrl;
}
@NonNull
@Override
public String toString() {
return "ModItem{" +
@@ -27,4 +30,8 @@ public class ModItem extends ModSource {
", isModpack=" + isModpack +
'}';
}
public String getIconCacheTag() {
return apiSource+"_"+id;
}
}

View File

@@ -1,11 +1,12 @@
package net.kdt.pojavlaunch.modloaders.modpacks.models;
import com.google.gson.annotations.SerializedName;
import androidx.annotation.NonNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Map;
/**
* POJO to represent the modrinth index inside mrpacks
@@ -20,6 +21,7 @@ public class ModrinthIndex {
public String summary;
public ModrinthIndexFile[] files;
public Map<String, String> dependencies;
public static class ModrinthIndexFile {
@@ -31,6 +33,7 @@ public class ModrinthIndex {
@Nullable public ModrinthIndexFileEnv env;
@NonNull
@Override
public String toString() {
return "ModrinthIndexFile{" +
@@ -45,6 +48,7 @@ public class ModrinthIndex {
public String sha1;
public String sha512;
@NonNull
@Override
public String toString() {
return "ModrinthIndexFileHashes{" +
@@ -58,6 +62,7 @@ public class ModrinthIndex {
public String client;
public String server;
@NonNull
@Override
public String toString() {
return "ModrinthIndexFileEnv{" +
@@ -68,27 +73,7 @@ public class ModrinthIndex {
}
}
public static class ModrinthIndexDependencies {
@Nullable public String minecraft;
@Nullable public String forge;
@SerializedName("fabric-loader")
@Nullable public String fabricLoader;
@SerializedName("quilt-loader")
@Nullable public String quiltLoader;
@Override
public String toString() {
return "ModrinthIndexDependencies{" +
"minecraft='" + minecraft + '\'' +
", forge='" + forge + '\'' +
", fabricLoader='" + fabricLoader + '\'' +
", quiltLoader='" + quiltLoader + '\'' +
'}';
}
}
@NonNull
@Override
public String toString() {
return "ModrinthIndex{" +

View File

@@ -0,0 +1,40 @@
package net.kdt.pojavlaunch.progresskeeper;
import static net.kdt.pojavlaunch.Tools.BYTE_TO_MB;
import net.kdt.pojavlaunch.Tools;
public class DownloaderProgressWrapper implements Tools.DownloaderFeedback {
private final int mProgressString;
private final String mProgressRecord;
public String extraString = null;
/**
* A simple wrapper to send the downloader progress to ProgressKeeper
* @param progressString the string that will be used in the progress reporter
* @param progressRecord the record for ProgressKeeper
*/
public DownloaderProgressWrapper(int progressString, String progressRecord) {
this.mProgressString = progressString;
this.mProgressRecord = progressRecord;
}
@Override
public void updateProgress(int curr, int max) {
Object[] va;
if(extraString != null) {
va = new Object[3];
va[0] = extraString;
va[1] = curr/BYTE_TO_MB;
va[2] = max/BYTE_TO_MB;
}
else {
va = new Object[2];
va[0] = curr/BYTE_TO_MB;
va[1] = max/BYTE_TO_MB;
}
// the allocations are fine because thats how java implements variadic arguments in bytecode: an array of whatever
ProgressKeeper.submitProgress(mProgressRecord, (int) Math.max((float)curr/max*100,0), mProgressString, va);
}
}

View File

@@ -1,6 +1,7 @@
package net.kdt.pojavlaunch.tasks;
import static net.kdt.pojavlaunch.PojavApplication.sExecutorService;
import static net.kdt.pojavlaunch.Tools.BYTE_TO_MB;
import static net.kdt.pojavlaunch.utils.DownloadUtils.downloadFileMonitored;
import android.app.Activity;
@@ -40,7 +41,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class AsyncMinecraftDownloader {
private static final float BYTE_TO_MB = 1024 * 1024;
public static final String MINECRAFT_RES = "https://resources.download.minecraft.net/";
/* Allows each downloading thread to have its own RECYCLED buffer */

View File

@@ -0,0 +1,58 @@
package net.kdt.pojavlaunch.utils;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class ZipUtils {
/**
* Gets an InputStream for a given ZIP entry, throwing an IOException if the ZIP entry does not
* exist.
* @param zipFile The ZipFile to get the entry from
* @param entryPath The full path inside of the ZipFile
* @return The InputStream provided by the ZipFile
* @throws IOException if the entry was not found
*/
public static InputStream getEntryStream(ZipFile zipFile, String entryPath) throws IOException{
ZipEntry entry = zipFile.getEntry(entryPath);
if(entry == null) throw new IOException("No entry in ZIP file: "+entryPath);
return zipFile.getInputStream(entry);
}
/**
* Extracts all files in a ZipFile inside of a given directory to a given destination directory
* How to specify dirName:
* If you want to extract all files in the ZipFile, specify ""
* If you want to extract a single directory, specify its full path followed by a trailing /
* @param zipFile The ZipFile to extract files from
* @param dirName The directory to extract the files from
* @param destination The destination directory to extract the files into
* @throws IOException if it was not possible to create a directory or file extraction failed
*/
public static void zipExtract(ZipFile zipFile, String dirName, File destination) throws IOException {
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
int dirNameLen = dirName.length();
while(zipEntries.hasMoreElements()) {
ZipEntry zipEntry = zipEntries.nextElement();
String entryName = zipEntry.getName();
if(!entryName.startsWith(dirName) || zipEntry.isDirectory()) continue;
File zipDestination = new File(destination, entryName.substring(dirNameLen));
File parent = zipDestination.getParentFile();
if(parent != null && !parent.exists())
if(!parent.mkdirs()) throw new IOException("Failed to create "+parent.getAbsolutePath());
try (InputStream inputStream = zipFile.getInputStream(zipEntry);
OutputStream outputStream =
new FileOutputStream(zipDestination)) {
IOUtils.copy(inputStream, outputStream);
}
}
}
}

View File

@@ -77,4 +77,15 @@
app:layout_constraintTop_toBottomOf="@id/search_mod_selected_mc_version_textview"
/>
<TextView
android:id="@+id/search_mod_status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/search_mod_list"
app:layout_constraintEnd_toEndOf="@+id/search_mod_list"
app:layout_constraintStart_toStartOf="@+id/search_mod_list"
app:layout_constraintTop_toTopOf="@+id/search_mod_list" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -410,5 +410,9 @@
<string name="generic_install">Install</string>
<string name="search_modpack_no_result">No modpacks found</string>
<string name="search_modpack_error">Failed to find modpacks</string>
<string name="modpack_download_downloading_metadata">Downloading modpack metadata (%.2f MB / %.2f MB)</string>
<string name="modpack_download_downloading_mods">Downloading mods (%.2f MB / %.2f MB)</string>
<string name="modpack_download_applying_overrides">Applying overrides (%d/%d)</string>
</resources>