From 88a6bcb6a59ac8efbc3547991b1ca872e51587f3 Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Sat, 31 May 2025 17:01:17 +0800 Subject: [PATCH 1/5] fix[DownloadMirror]: Make scheduleDownload() fallback to file counter when can't connect --- .../pojavlaunch/mirrors/DownloadMirror.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java index 2cced7b7e..566f87a6c 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java @@ -76,14 +76,18 @@ public class DownloadMirror { * @param urlInput The original (Mojang) URL for the download * @return the length of the file denoted by the URL in bytes, or -1 if not available */ - public static long getContentLengthMirrored(int downloadClass, String urlInput) throws IOException { - long length = DownloadUtils.getContentLength(getMirrorMapping(downloadClass, urlInput)); - if(length < 1) { - Log.w("DownloadMirror", "Unable to get content length from mirror"); - Log.i("DownloadMirror", "Falling back to default source"); - return DownloadUtils.getContentLength(urlInput); - }else { - return length; + public static long getContentLengthMirrored(int downloadClass, String urlInput){ + try { + long length = DownloadUtils.getContentLength(getMirrorMapping(downloadClass, urlInput)); + if (length < 1) { + Log.w("DownloadMirror", "Unable to get content length from mirror"); + Log.i("DownloadMirror", "Falling back to default source"); + return DownloadUtils.getContentLength(urlInput); + } else { + return length; + } + } catch (IOException ignored) { // If error happens, fallback to old file counter instead of size. This shouldn't really happen unless offline though. + return -1L; } } From 4170e090f73dce1f843e1f577cfa74c44a5e6090 Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Sat, 31 May 2025 17:17:21 +0800 Subject: [PATCH 2/5] fix[MinecraftDownloader]: Implement caching SHA1 hashes for offline use --- .../main/java/net/kdt/pojavlaunch/Tools.java | 14 +++++ .../tasks/MinecraftDownloader.java | 53 ++++++++++++++++--- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java index 24bd00d33..8ed093b26 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -21,6 +21,8 @@ import android.content.res.Resources; import android.database.Cursor; import android.hardware.Sensor; import android.hardware.SensorManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -1429,6 +1431,18 @@ public final class Tools { OBSOLETE_RESOURCES_PATH = DIR_GAME_NEW + "/resources"; } + private static NetworkInfo getActiveNetworkInfo(Context ctx) { + ConnectivityManager connMgr = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); + return networkInfo; // This can return null when there is no wifi or data connected + } + + public static boolean isOnline(Context ctx) { + NetworkInfo info = getActiveNetworkInfo(ctx); + if(info == null) return false; + return (info.isConnected()); + } + public static boolean isDemoProfile(Context ctx){ MinecraftAccount currentProfile = PojavProfile.getCurrentProfileContent(ctx, null); return currentProfile != null && currentProfile.isDemo(); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloader.java index 70a4f0834..db34eee3e 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloader.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloader.java @@ -3,6 +3,9 @@ package net.kdt.pojavlaunch.tasks; import static net.kdt.pojavlaunch.PojavApplication.sExecutorService; import android.app.Activity; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.util.Log; import androidx.annotation.NonNull; @@ -25,9 +28,12 @@ import net.kdt.pojavlaunch.value.DependentLibrary; import net.kdt.pojavlaunch.value.MinecraftClientInfo; import net.kdt.pojavlaunch.value.MinecraftLibraryArtifact; +import java.io.BufferedReader; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; import java.io.IOException; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Map; import java.util.Set; @@ -56,6 +62,7 @@ public class MinecraftDownloader { private static final ThreadLocal sThreadLocalDownloadBuffer = new ThreadLocal<>(); private boolean isLocalProfile = false; + private boolean isOnline; /** * Start the game version download process on the global executor service. @@ -69,7 +76,9 @@ public class MinecraftDownloader { @NonNull AsyncMinecraftDownloader.DoneListener listener) { if(activity != null){ isLocalProfile = Tools.isLocalProfile(activity); + isOnline = Tools.isOnline(activity); Tools.switchDemo(Tools.isDemoProfile(activity)); + } else { isLocalProfile = true; Tools.switchDemo(true); @@ -82,9 +91,6 @@ public class MinecraftDownloader { } downloadGame(activity, version, realVersion); listener.onDownloadDone(); - }catch (UnknownHostException e){ - Log.i("DownloadMirror", e.toString()); - Tools.showErrorRemote("Can't download Minecraft, no internet connection found", e); }catch (Exception e) { listener.onDownloadFailed(e); } @@ -473,18 +479,49 @@ public class MinecraftDownloader { * Since Minecraft libraries are stored in maven repositories, try to use * this when downloading libraries without hashes in the json. */ - private void tryGetLibrarySha1() { + private void tryGetLibrarySha1() throws IOException { + File sha1CacheDir = new File(Tools.DIR_CACHE + "/sha1hashes"); + File cacheFile = new File(sha1CacheDir.getAbsolutePath() + FileUtils.getFileName(mTargetUrl) + ".sha"); + + // Only use cache when its offline. No point in having cache invalidation now! + if (!isOnline) { + try (BufferedReader cacheFileReader = new BufferedReader(new FileReader(cacheFile))) { + mTargetSha1 = cacheFileReader.readLine(); + if (mTargetSha1 != null) { + Log.i("MinecraftDownloader", "(No internet found) Reading Hash: " + mTargetSha1 + " from " + cacheFile); + } else if (cacheFile.exists()) { + Log.i("MinecraftDownloader", "(No internet found) Deleting invalid hash from cache: " + cacheFile); + cacheFile.delete(); + } + } catch (FileNotFoundException ignored) { + mTargetSha1 = null; + Log.w("MinecraftDownloader", "(No internet found) Failed to read hash for " + cacheFile); + } + return; + } + String resultHash = null; try { resultHash = downloadSha1(); // The hash is a 40-byte download. mInternetUsageCounter.getAndAdd(40); - }catch (IOException e) { + } catch (IOException e) { Log.i("MinecraftDownloader", "Failed to download hash", e); + if (cacheFile.exists() && new BufferedReader(new FileReader(cacheFile)).readLine() == null) { + Log.i("MinecraftDownloader", "Deleting failed hash download from cache: " + cacheFile); + cacheFile.delete(); + } } - if(resultHash != null) { - Log.i("MinecraftDownloader", "Got hash: "+resultHash+ " for "+FileUtils.getFileName(mTargetUrl)); + if (resultHash != null) { + Log.i("MinecraftDownloader", "Got hash: " + resultHash + " for " + FileUtils.getFileName(mTargetUrl)); mTargetSha1 = resultHash; + if (!sha1CacheDir.exists()) { + sha1CacheDir.mkdir(); // If mkdir() fails, something went wrong with initializing /data/data/. mkdirs() isn't used on purpose + } + try (FileWriter writeHash = new FileWriter(cacheFile)) { + Log.i("MinecraftDownloader", "Saving hash: " + resultHash + " for " + FileUtils.getFileName(mTargetUrl) + " to " + cacheFile); + writeHash.write(resultHash); + } } } From df5e2067da9bb62136c98e8a75f9cefb61a4c6de Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Sun, 1 Jun 2025 15:28:26 +0800 Subject: [PATCH 3/5] qol[mcAccountSpinner]: Remove error that appears when opening app without internet --- .../src/main/java/com/kdt/mcgui/mcAccountSpinner.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcAccountSpinner.java b/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcAccountSpinner.java index 0c576ade2..5f5d637e4 100644 --- a/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcAccountSpinner.java +++ b/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcAccountSpinner.java @@ -9,6 +9,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.net.NetworkInfo; import android.net.Uri; import android.util.AttributeSet; import android.util.Log; @@ -276,6 +277,10 @@ public class mcAccountSpinner extends AppCompatSpinner implements AdapterView.On } private void performLogin(MinecraftAccount minecraftAccount){ + // Logging in when there's no internet is useless. This should really be turned into a network callback though. + if(!Tools.isOnline(getContext())){ + return; + } if(minecraftAccount.isLocal()) return; mLoginBarPaint.setColor(getResources().getColor(R.color.minebutton_color)); From c5e4d7e0271436adbc42277d077a4c47ed94c15c Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Sun, 1 Jun 2025 21:34:12 +0800 Subject: [PATCH 4/5] fix[DownloadUtils]: When download fails, delete 0 byte file made by FileOutputSteam --- .../main/java/net/kdt/pojavlaunch/utils/DownloadUtils.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/DownloadUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/DownloadUtils.java index 0c4d35eed..0f3bc3e96 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/DownloadUtils.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/DownloadUtils.java @@ -61,6 +61,12 @@ public class DownloadUtils { FileUtils.ensureParentDirectory(out); try (FileOutputStream fileOutputStream = new FileOutputStream(out)) { download(url, fileOutputStream); + } catch (IOException e) { + if (out.length() < 1) { // Only delete it if file is 0 bytes cause this file might already be downloaded and something else went wrong. + Log.i("DownloadUtils", "Cleaning up failed download: " + out.getAbsolutePath()); + out.delete(); + throw e; + } } } From 951b35ca5f785c9935002ed8ef4ba997706d46e6 Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Sun, 8 Jun 2025 17:31:30 +0800 Subject: [PATCH 5/5] fix[MinecraftDownloader]: Fix local profile and offline handling so they can actually launch the game --- .../tasks/MinecraftDownloader.java | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloader.java index db34eee3e..40abf1723 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloader.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloader.java @@ -72,7 +72,7 @@ public class MinecraftDownloader { * @param listener The download status listener */ public void start(@Nullable Activity activity, @Nullable JMinecraftVersionList.Version version, - @NonNull String realVersion, // this was there for a reason + @NonNull String realVersion, @NonNull AsyncMinecraftDownloader.DoneListener listener) { if(activity != null){ isLocalProfile = Tools.isLocalProfile(activity); @@ -86,11 +86,32 @@ public class MinecraftDownloader { sExecutorService.execute(() -> { try { - if(isLocalProfile){ - throw new RuntimeException("Download failed. Please make sure you are logged in with a Microsoft Account."); - } + if(isLocalProfile || !isOnline) { + String versionMessage = realVersion; // Use provided version unless we find its a modded instance + + // See if provided version is a modded version and if that version depends on another jar, check for presence of both jar's .json. + try { + // This reads the .json associated with the provided version. If it fails, we can assume it's not installed. + File providedJsonFile = new File(Tools.DIR_HOME_VERSION + "/" + realVersion + "/" + realVersion + ".json"); + JMinecraftVersionList.Version providedJson = Tools.GLOBAL_GSON.fromJson(Tools.read(providedJsonFile.getAbsolutePath()), JMinecraftVersionList.Version.class); + + // This checks if running modded version that depends on other jars, so we use that for the error message. + File vanillaJsonFile = new File(Tools.DIR_HOME_VERSION + "/" + providedJson.inheritsFrom + "/" + providedJson.inheritsFrom + ".json"); + versionMessage = providedJson.inheritsFrom != null ? providedJson.inheritsFrom : versionMessage; + + // Ensure they're both not some 0 byte corrupted json + if (providedJsonFile.length() == 0 || vanillaJsonFile.exists() && vanillaJsonFile.length() == 0){ + throw new RuntimeException("Minecraft "+versionMessage+ " is needed by " +realVersion); } + + listener.onDownloadDone(); + } catch (Exception e) { + String tryagain = !isOnline ? "Please ensure you have an internet connection" : "Please try again on your Microsoft Account"; + Tools.showErrorRemote(versionMessage + " is not currently installed. "+ tryagain, e); + } + }else { downloadGame(activity, version, realVersion); listener.onDownloadDone(); + } }catch (Exception e) { listener.onDownloadFailed(e); } @@ -484,18 +505,18 @@ public class MinecraftDownloader { File cacheFile = new File(sha1CacheDir.getAbsolutePath() + FileUtils.getFileName(mTargetUrl) + ".sha"); // Only use cache when its offline. No point in having cache invalidation now! - if (!isOnline) { + if (!isOnline || !LauncherPreferences.PREF_CHECK_LIBRARY_SHA) { // Well not only offlines..this setting speeds up launch times at least! try (BufferedReader cacheFileReader = new BufferedReader(new FileReader(cacheFile))) { mTargetSha1 = cacheFileReader.readLine(); if (mTargetSha1 != null) { - Log.i("MinecraftDownloader", "(No internet found) Reading Hash: " + mTargetSha1 + " from " + cacheFile); + Log.i("MinecraftDownloader", "Reading Hash from cache: " + mTargetSha1 + " from " + cacheFile); } else if (cacheFile.exists()) { - Log.i("MinecraftDownloader", "(No internet found) Deleting invalid hash from cache: " + cacheFile); + Log.i("MinecraftDownloader", "Deleting invalid hash from cache: " + cacheFile); cacheFile.delete(); } } catch (FileNotFoundException ignored) { mTargetSha1 = null; - Log.w("MinecraftDownloader", "(No internet found) Failed to read hash for " + cacheFile); + Log.w("MinecraftDownloader", "Failed to read hash for " + cacheFile); } return; }