From 5181a78cfc6347976319a73f307eb609ce5f3422 Mon Sep 17 00:00:00 2001 From: Vera-Firefly <87926662+Vera-Firefly@users.noreply.github.com> Date: Mon, 28 Aug 2023 16:57:17 +0800 Subject: [PATCH] Update --- app_pojavlauncher/build.gradle | 12 + .../src/main/AndroidManifest.xml | 5 + .../forge_installer/forge_installer.jar | Bin 79680 -> 80814 bytes .../assets/components/forge_installer/version | 2 +- .../main/java/com/kdt/SimpleArrayAdapter.java | 67 +++ .../java/com/kdt/mcgui/mcVersionSpinner.java | 5 + .../java/net/kdt/pojavlaunch/JRE17Util.java | 6 +- .../net/kdt/pojavlaunch/LauncherActivity.java | 21 + .../net/kdt/pojavlaunch/MainActivity.java | 12 +- .../net/kdt/pojavlaunch/PojavApplication.java | 12 +- .../kdt/pojavlaunch/ShowErrorActivity.java | 75 ++++ .../main/java/net/kdt/pojavlaunch/Tools.java | 54 ++- .../contextexecutor/ContextExecutor.java | 75 ++++ .../contextexecutor/ContextExecutorTask.java | 25 ++ .../fragments/FabricInstallFragment.java | 4 +- .../fragments/ForgeInstallFragment.java | 2 +- .../fragments/MainMenuFragment.java | 15 +- .../fragments/OptiFineInstallFragment.java | 3 +- .../fragments/ProfileEditorFragment.java | 4 +- .../fragments/ProfileTypeSelectFragment.java | 2 + .../fragments/SearchModFragment.java | 165 +++++++ .../modloaders/FabricDownloadTask.java | 6 +- .../modloaders/ForgeDownloadTask.java | 78 +++- .../pojavlaunch/modloaders/ForgeUtils.java | 6 + .../modloaders/OptiFineDownloadTask.java | 6 +- .../modloaders/modpacks/ModItemAdapter.java | 409 ++++++++++++++++++ .../modpacks/ModloaderInstallTracker.java | 106 +++++ .../modpacks/SelfReferencingFuture.java | 40 ++ .../modloaders/modpacks/api/ApiHandler.java | 164 +++++++ .../modloaders/modpacks/api/CommonApi.java | 187 ++++++++ .../modpacks/api/CurseforgeApi.java | 242 +++++++++++ .../modpacks/api/ModDownloader.java | 172 ++++++++ .../modloaders/modpacks/api/ModLoader.java | 87 ++++ .../modloaders/modpacks/api/ModpackApi.java | 73 ++++ .../modpacks/api/ModpackInstaller.java | 63 +++ .../modloaders/modpacks/api/ModrinthApi.java | 139 ++++++ .../api/NotificationDownloadListener.java | 67 +++ .../imagecache/DownloadImageTask.java | 61 +++ .../modpacks/imagecache/IconCacheJanitor.java | 86 ++++ .../modpacks/imagecache/ImageReceiver.java | 10 + .../modpacks/imagecache/ModIconCache.java | 109 +++++ .../modpacks/imagecache/ReadFromDiskTask.java | 55 +++ .../modloaders/modpacks/models/Constants.java | 16 + .../modpacks/models/CurseManifest.java | 25 ++ .../modloaders/modpacks/models/ModDetail.java | 41 ++ .../modloaders/modpacks/models/ModItem.java | 37 ++ .../modloaders/modpacks/models/ModSource.java | 6 + .../modpacks/models/ModrinthIndex.java | 89 ++++ .../modpacks/models/SearchFilters.java | 13 + .../modpacks/models/SearchResult.java | 6 + .../pojavlaunch/profiles/ProfileAdapter.java | 25 +- .../DownloaderProgressWrapper.java | 40 ++ .../kdt/pojavlaunch/services/GameService.java | 6 +- .../pojavlaunch/services/ProgressService.java | 6 +- .../tasks/AsyncMinecraftDownloader.java | 3 +- .../net/kdt/pojavlaunch/utils/FileUtils.java | 11 + .../net/kdt/pojavlaunch/utils/ZipUtils.java | 58 +++ .../value/NotificationConstants.java | 12 + .../launcherprofiles/LauncherProfiles.java | 58 +-- .../src/main/jni/ctxbridges/osmesa_loader.c | 31 +- .../src/main/jni/ctxbridges/osmesa_loader.h | 3 +- app_pojavlauncher/src/main/jni/egl_bridge.c | 3 + .../main/res/drawable/background_overlay.xml | 11 + .../src/main/res/drawable/ic_curseforge.png | Bin 0 -> 5502 bytes .../src/main/res/drawable/ic_filter.xml | 9 + .../src/main/res/drawable/ic_modrinth.png | Bin 0 -> 6229 bytes .../main/res/layout/dialog_mod_filters.xml | 61 +++ .../main/res/layout/fragment_mod_search.xml | 105 +++++ .../main/res/layout/fragment_profile_type.xml | 8 + .../src/main/res/layout/view_loading.xml | 12 + .../src/main/res/layout/view_mod.xml | 83 ++++ .../src/main/res/layout/view_mod_extended.xml | 40 ++ .../src/main/res/values/colors.xml | 1 + .../src/main/res/values/strings.xml | 25 +- .../git/artdeell/installer_agent/Agent.java | 29 +- .../installer_agent/ProfileFixer.java | 43 +- 76 files changed, 3544 insertions(+), 104 deletions(-) create mode 100644 app_pojavlauncher/src/main/java/com/kdt/SimpleArrayAdapter.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModItemAdapter.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModloaderInstallTracker.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/SelfReferencingFuture.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ApiHandler.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModDownloader.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoader.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackInstaller.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/NotificationDownloadListener.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/DownloadImageTask.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/IconCacheJanitor.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ImageReceiver.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ModIconCache.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ReadFromDiskTask.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/Constants.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/CurseManifest.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModItem.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModSource.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModrinthIndex.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchFilters.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchResult.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/progresskeeper/DownloaderProgressWrapper.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/ZipUtils.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/NotificationConstants.java create mode 100644 app_pojavlauncher/src/main/res/drawable/background_overlay.xml create mode 100644 app_pojavlauncher/src/main/res/drawable/ic_curseforge.png create mode 100644 app_pojavlauncher/src/main/res/drawable/ic_filter.xml create mode 100644 app_pojavlauncher/src/main/res/drawable/ic_modrinth.png create mode 100644 app_pojavlauncher/src/main/res/layout/dialog_mod_filters.xml create mode 100644 app_pojavlauncher/src/main/res/layout/fragment_mod_search.xml create mode 100644 app_pojavlauncher/src/main/res/layout/view_loading.xml create mode 100644 app_pojavlauncher/src/main/res/layout/view_mod.xml create mode 100644 app_pojavlauncher/src/main/res/layout/view_mod_extended.xml diff --git a/app_pojavlauncher/build.gradle b/app_pojavlauncher/build.gradle index 8cc5e59c6..8dc7417ed 100644 --- a/app_pojavlauncher/build.gradle +++ b/app_pojavlauncher/build.gradle @@ -67,6 +67,17 @@ def getVersionName = { return TAG_STRING.trim().replace("-g", "-") + "-" + BRANCH.toString().trim() } +def getCFApiKey = { + String key = System.getenv("CURSEFORGE_API_KEY"); + if(key != null) return key; + File curseforgeKeyFile = new File("./curseforge_key.txt"); + if(curseforgeKeyFile.canRead() && curseforgeKeyFile.isFile()) { + return curseforgeKeyFile.text; + } + logger.warn('BUILD: You have no CurseForge key, the curseforge api will get disabled !'); + return "DUMMY"; +} + configurations { instrumentedClasspath { canBeConsumed = false @@ -105,6 +116,7 @@ android { versionCode getDateSeconds() versionName getVersionName() multiDexEnabled true //important + resValue 'string', 'curseforge_api_key', getCFApiKey() } buildTypes { diff --git a/app_pojavlauncher/src/main/AndroidManifest.xml b/app_pojavlauncher/src/main/AndroidManifest.xml index 457e0cbfd..4147e1f3a 100644 --- a/app_pojavlauncher/src/main/AndroidManifest.xml +++ b/app_pojavlauncher/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ android:name="android.hardware.type.pc" android:required="false" /> + @@ -77,11 +78,15 @@ android:name=".FatalErrorActivity" android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation" android:theme="@style/Theme.AppCompat.DayNight.Dialog" /> + diff --git a/app_pojavlauncher/src/main/assets/components/forge_installer/forge_installer.jar b/app_pojavlauncher/src/main/assets/components/forge_installer/forge_installer.jar index 2e498a39b30888990bf7dfead827eb7da41efdb6..c540bee9c39d85c418898030383676067bbbc39f 100644 GIT binary patch delta 6605 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 delta 5427 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 mSMe*1svwI1 diff --git a/app_pojavlauncher/src/main/assets/components/forge_installer/version b/app_pojavlauncher/src/main/assets/components/forge_installer/version index 825b64780..50f426278 100644 --- a/app_pojavlauncher/src/main/assets/components/forge_installer/version +++ b/app_pojavlauncher/src/main/assets/components/forge_installer/version @@ -1 +1 @@ -1688133008591 \ No newline at end of file +1692525087345 \ No newline at end of file diff --git a/app_pojavlauncher/src/main/java/com/kdt/SimpleArrayAdapter.java b/app_pojavlauncher/src/main/java/com/kdt/SimpleArrayAdapter.java new file mode 100644 index 000000000..4069ae429 --- /dev/null +++ b/app_pojavlauncher/src/main/java/com/kdt/SimpleArrayAdapter.java @@ -0,0 +1,67 @@ +package com.kdt; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.List; + +/** + * Basic adapter, expect it uses the what is passed by the code, no the resources + * @param + */ +public class SimpleArrayAdapter extends BaseAdapter { + private List mObjects; + public SimpleArrayAdapter(List objects) { + setObjects(objects); + } + + public void setObjects(@Nullable List objects) { + if(objects == null){ + if(mObjects != Collections.emptyList()) { + mObjects = Collections.emptyList(); + notifyDataSetChanged(); + } + } else { + if(objects != mObjects){ + mObjects = objects; + notifyDataSetChanged(); + } + } + } + + @Override + public int getCount() { + return mObjects.size(); + } + + @Override + public T getItem(int position) { + return mObjects.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + if(convertView == null){ + convertView = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false); + } + + TextView v = (TextView) convertView; + v.setText(mObjects.get(position).toString()); + return v; + } +} diff --git a/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcVersionSpinner.java b/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcVersionSpinner.java index d0cd464c2..ae2e66f88 100644 --- a/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcVersionSpinner.java +++ b/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcVersionSpinner.java @@ -77,6 +77,11 @@ public void setSelection(int position){ mProfileAdapter.setViewProfile(this, (String) mProfileAdapter.getItem(position), false); } + /** Reload profiles from the file, forcing the spinner to consider the new data */ + public void reloadProfiles(){ + mProfileAdapter.reloadProfiles(); + } + /** Initialize various behaviors */ private void init(){ // Setup various attributes diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JRE17Util.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JRE17Util.java index f004f1307..1cd0dca35 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JRE17Util.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JRE17Util.java @@ -55,7 +55,7 @@ public static boolean installNewJreIfNeeded(Activity activity, JMinecraftVersion if (versionInfo.javaVersion == null || versionInfo.javaVersion.component.equalsIgnoreCase("jre-legacy")) return true; - LauncherProfiles.update(); + LauncherProfiles.load(); MinecraftProfile minecraftProfile = LauncherProfiles.getCurrentProfile(); String selectedRuntime = Tools.getSelectedRuntime(minecraftProfile); @@ -71,7 +71,7 @@ public static boolean installNewJreIfNeeded(Activity activity, JMinecraftVersion JRE17Util.checkInternalNewJre(activity.getAssets()); } minecraftProfile.javaDir = Tools.LAUNCHERPROFILES_RTPREFIX + appropriateRuntime; - LauncherProfiles.update(); + LauncherProfiles.load(); } else { if (versionInfo.javaVersion.majorVersion <= 17) { // there's a chance we have an internal one for this case if (!JRE17Util.checkInternalNewJre(activity.getAssets())){ @@ -79,7 +79,7 @@ public static boolean installNewJreIfNeeded(Activity activity, JMinecraftVersion return false; } else { minecraftProfile.javaDir = Tools.LAUNCHERPROFILES_RTPREFIX + JRE17Util.NEW_JRE_NAME; - LauncherProfiles.update(); + LauncherProfiles.load(); } } else { showRuntimeFail(activity, versionInfo); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java index 76a05254d..cbe045315 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java @@ -24,6 +24,7 @@ import com.kdt.mcgui.ProgressLayout; import com.kdt.mcgui.mcAccountSpinner; +import net.kdt.pojavlaunch.contextexecutor.ContextExecutor; import net.kdt.pojavlaunch.fragments.MainMenuFragment; import net.kdt.pojavlaunch.fragments.MicrosoftLoginFragment; import net.kdt.pojavlaunch.extra.ExtraConstants; @@ -31,6 +32,8 @@ import net.kdt.pojavlaunch.extra.ExtraListener; import net.kdt.pojavlaunch.fragments.SelectAuthFragment; +import net.kdt.pojavlaunch.modloaders.modpacks.ModloaderInstallTracker; +import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.IconCacheJanitor; import net.kdt.pojavlaunch.multirt.MultiRTConfigDialog; import net.kdt.pojavlaunch.prefs.LauncherPreferences; import net.kdt.pojavlaunch.prefs.screens.LauncherPreferenceFragment; @@ -53,6 +56,7 @@ public class LauncherActivity extends BaseActivity { private ImageButton mSettingsButton, mDeleteAccountButton; private ProgressLayout mProgressLayout; private ProgressServiceKeeper mProgressServiceKeeper; + private ModloaderInstallTracker mInstallTracker; /* Allows to switch from one button "type" to another */ private final FragmentManager.FragmentLifecycleCallbacks mFragmentCallbackListener = new FragmentManager.FragmentLifecycleCallbacks() { @@ -152,6 +156,7 @@ public void onDownloadFailed(Throwable th) { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_pojav_launcher); + IconCacheJanitor.runJanitor(); getWindow().setBackgroundDrawable(null); bindViews(); ProgressKeeper.addTaskCountListener((mProgressServiceKeeper = new ProgressServiceKeeper(this))); @@ -167,6 +172,8 @@ protected void onCreate(Bundle savedInstanceState) { new AsyncVersionList().getVersionList(versions -> ExtraCore.setValue(ExtraConstants.RELEASE_TABLE, versions), false); + mInstallTracker = new ModloaderInstallTracker(this); + mProgressLayout.observe(ProgressLayout.DOWNLOAD_MINECRAFT); mProgressLayout.observe(ProgressLayout.UNPACK_RUNTIME); mProgressLayout.observe(ProgressLayout.INSTALL_MODPACK); @@ -174,6 +181,20 @@ protected void onCreate(Bundle savedInstanceState) { mProgressLayout.observe(ProgressLayout.DOWNLOAD_VERSION_LIST); } + @Override + protected void onResume() { + super.onResume(); + ContextExecutor.setActivity(this); + mInstallTracker.attach(); + } + + @Override + protected void onPause() { + super.onPause(); + ContextExecutor.clearActivity(); + mInstallTracker.detach(); + } + @Override public boolean setFullscreen() { return false; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java index 3b6d8abff..dfad0b9f1 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java @@ -322,24 +322,24 @@ private void runCraft(String versionId, JMinecraftVersionList.Version version) t } MinecraftAccount minecraftAccount = PojavProfile.getCurrentProfileContent(this, null); Logger.appendToLog("--------- beginning with launcher debug"); - printLauncherInfo(versionId); + printLauncherInfo(versionId, Tools.isValidString(minecraftProfile.javaArgs) ? minecraftProfile.javaArgs : LauncherPreferences.PREF_CUSTOM_JAVA_ARGS); if (Tools.LOCAL_RENDERER.equals("vulkan_zink")) { checkVulkanZinkIsSupported(); } JREUtils.redirectAndPrintJRELog(); - LauncherProfiles.update(); + LauncherProfiles.load(); int requiredJavaVersion = 8; if(version.javaVersion != null) requiredJavaVersion = version.javaVersion.majorVersion; Tools.launchMinecraft(this, minecraftAccount, minecraftProfile, versionId, requiredJavaVersion); } - private void printLauncherInfo(String gameVersion) { + private void printLauncherInfo(String gameVersion, String javaArguments) { Logger.appendToLog("Info: Launcher version: " + BuildConfig.VERSION_NAME); Logger.appendToLog("Info: Architecture: " + Architecture.archAsString(Tools.DEVICE_ARCHITECTURE)); Logger.appendToLog("Info: Device model: " + Build.MANUFACTURER + " " +Build.MODEL); Logger.appendToLog("Info: API version: " + Build.VERSION.SDK_INT); Logger.appendToLog("Info: Selected Minecraft version: " + gameVersion); - Logger.appendToLog("Info: Custom Java arguments: \"" + LauncherPreferences.PREF_CUSTOM_JAVA_ARGS + "\""); + Logger.appendToLog("Info: Custom Java arguments: \"" + javaArguments + "\""); } private void checkVulkanZinkIsSupported() { @@ -430,12 +430,12 @@ public void adjustMouseSpeedLive() { sb.setMax(275); tmpMouseSpeed = (int) ((LauncherPreferences.PREF_MOUSESPEED*100)); sb.setProgress(tmpMouseSpeed-25); - tv.setText(getString(R.string.percent_format, tmpGyroSensitivity)); + tv.setText(getString(R.string.percent_format, tmpMouseSpeed)); sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { tmpMouseSpeed = i+25; - tv.setText(getString(R.string.percent_format, tmpGyroSensitivity)); + tv.setText(getString(R.string.percent_format, tmpMouseSpeed)); } @Override public void onStartTrackingTouch(SeekBar seekBar) {} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java index ee67bca05..09a22a2ba 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java @@ -18,6 +18,7 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import net.kdt.pojavlaunch.contextexecutor.ContextExecutor; import net.kdt.pojavlaunch.tasks.AsyncAssetManager; import net.kdt.pojavlaunch.utils.*; @@ -27,6 +28,7 @@ public class PojavApplication extends Application { @Override public void onCreate() { + ContextExecutor.setApplication(this); Thread.setDefaultUncaughtExceptionHandler((thread, th) -> { boolean storagePermAllowed = (Build.VERSION.SDK_INT < 23 || Build.VERSION.SDK_INT >= 29 || ActivityCompat.checkSelfPermission(PojavApplication.this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) && Tools.checkStorageRoot(PojavApplication.this); @@ -78,8 +80,14 @@ public void onCreate() { startActivity(ferrorIntent); } } - - @Override + + @Override + public void onTerminate() { + super.onTerminate(); + ContextExecutor.clearApplication(); + } + + @Override protected void attachBaseContext(Context base) { super.attachBaseContext(LocaleUtils.setLocale(base)); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java new file mode 100644 index 000000000..ec64ee7fb --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java @@ -0,0 +1,75 @@ +package net.kdt.pojavlaunch; + +import android.app.Activity; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; + +import net.kdt.pojavlaunch.contextexecutor.ContextExecutorTask; +import net.kdt.pojavlaunch.value.NotificationConstants; + +import java.io.Serializable; + +public class ShowErrorActivity extends Activity { + + private static final String ERROR_ACTIVITY_REMOTE_TASK = "remoteTask"; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + if(intent == null) { + finish(); + return; + } + RemoteErrorTask remoteErrorTask = (RemoteErrorTask) intent.getSerializableExtra(ERROR_ACTIVITY_REMOTE_TASK); + if(remoteErrorTask == null) { + finish(); + return; + } + remoteErrorTask.executeWithActivity(this); + } + + + public static class RemoteErrorTask implements ContextExecutorTask, Serializable { + private final Throwable mThrowable; + private final String mRolledMsg; + + public RemoteErrorTask(Throwable mThrowable, String mRolledMsg) { + this.mThrowable = mThrowable; + this.mRolledMsg = mRolledMsg; + } + @Override + public void executeWithActivity(Activity activity) { + Tools.showError(activity, mRolledMsg, mThrowable); + } + + @Override + public void executeWithApplication(Context context) { + sendNotification(context, this); + } + } + private static void sendNotification(Context context, RemoteErrorTask remoteErrorTask) { + + Intent showErrorIntent = new Intent(context, ShowErrorActivity.class); + showErrorIntent.putExtra(ERROR_ACTIVITY_REMOTE_TASK, remoteErrorTask); + + PendingIntent pendingIntent = PendingIntent.getActivity(context, NotificationConstants.PENDINGINTENT_CODE_SHOW_ERROR, showErrorIntent, + Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0); + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, context.getString(R.string.notif_channel_id)) + .setContentTitle(context.getString(R.string.notif_error_occured)) + .setContentText(context.getString(R.string.notif_error_occured_desc)) + .setSmallIcon(R.drawable.notif_icon) + .setContentIntent(pendingIntent); + notificationManager.notify(NotificationConstants.NOTIFICATION_ID_SHOW_ERROR, notificationBuilder.build()); + } + +} 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 9a5392ee7..e2f7d711b 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -33,6 +33,7 @@ import android.view.WindowManager; import android.webkit.MimeTypeMap; import android.widget.EditText; +import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -45,6 +46,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import net.kdt.pojavlaunch.contextexecutor.ContextExecutor; import net.kdt.pojavlaunch.multirt.MultiRTUtils; import net.kdt.pojavlaunch.multirt.Runtime; import net.kdt.pojavlaunch.plugins.FFmpegPlugin; @@ -55,6 +57,7 @@ import net.kdt.pojavlaunch.utils.OldVersionsUtils; import net.kdt.pojavlaunch.value.DependentLibrary; import net.kdt.pojavlaunch.value.MinecraftAccount; +import net.kdt.pojavlaunch.value.MinecraftLibraryArtifact; import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; @@ -79,6 +82,7 @@ @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"; @@ -173,7 +177,7 @@ public static void launchMinecraft(final Activity activity, MinecraftAccount min } Runtime runtime = MultiRTUtils.forceReread(Tools.pickRuntime(minecraftProfile, versionJavaRequirement)); JMinecraftVersionList.Version versionInfo = Tools.getVersionInfo(versionId); - LauncherProfiles.update(); + LauncherProfiles.load(); File gamedir = Tools.getGameDirPath(minecraftProfile); @@ -592,6 +596,23 @@ private static void showError(final Context ctx, final int titleId, final String } } + public static void showErrorRemote(Throwable e) { + showErrorRemote(null, e); + } + public static void showErrorRemote(Context context, int rolledMessage, Throwable e) { + showErrorRemote(context.getString(rolledMessage), e); + } + public static void showErrorRemote(String rolledMessage, Throwable e) { + // I WILL embrace layer violations because Android's concept of layers is STUPID + // We live in the same process anyway, why make it any more harder with this needless + // abstraction? + + // Add your Context-related rage here + ContextExecutor.execute(new ShowErrorActivity.RemoteErrorTask(e, rolledMessage)); + } + + + public static void dialogOnUiThread(final Activity activity, final CharSequence title, final CharSequence message) { activity.runOnUiThread(()->dialog(activity, title, message)); } @@ -626,8 +647,9 @@ private static void preProcessLibraries(DependentLibrary[] libraries) { if (libItem.name.startsWith("net.java.dev.jna:jna:")) { // Special handling for LabyMod 1.8.9, Forge 1.12.2(?) and oshi // we have libjnidispatch 5.13.0 in jniLibs directory - if (Integer.parseInt(version[0]) >= 5 && Integer.parseInt(version[1]) >= 13) return; + if (Integer.parseInt(version[0]) >= 5 && Integer.parseInt(version[1]) >= 13) continue; Log.d(APP_NAME, "Library " + libItem.name + " has been changed to version 5.13.0"); + createLibraryInfo(libItem); libItem.name = "net.java.dev.jna:jna:5.13.0"; libItem.downloads.artifact.path = "net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar"; libItem.downloads.artifact.sha1 = "1200e7ebeedbe0d10062093f32925a912020e747"; @@ -636,16 +658,34 @@ private static void preProcessLibraries(DependentLibrary[] libraries) { //if (Integer.parseInt(version[0]) >= 6 && Integer.parseInt(version[1]) >= 3) return; // FIXME: ensure compatibility - if (Integer.parseInt(version[0]) != 6 || Integer.parseInt(version[1]) != 2) return; + if (Integer.parseInt(version[0]) != 6 || Integer.parseInt(version[1]) != 2) continue; Log.d(APP_NAME, "Library " + libItem.name + " has been changed to version 6.3.0"); + createLibraryInfo(libItem); libItem.name = "com.github.oshi:oshi-core:6.3.0"; libItem.downloads.artifact.path = "com/github/oshi/oshi-core/6.3.0/oshi-core-6.3.0.jar"; libItem.downloads.artifact.sha1 = "9e98cf55be371cafdb9c70c35d04ec2a8c2b42ac"; libItem.downloads.artifact.url = "https://repo1.maven.org/maven2/com/github/oshi/oshi-core/6.3.0/oshi-core-6.3.0.jar"; + } else if (libItem.name.startsWith("org.ow2.asm:asm-all:")) { + // Early versions of the ASM library get repalced with 5.0.4 because Pojav's LWJGL is compiled for + // Java 8, which is not supported by old ASM versions. Mod loaders like Forge, which depend on this + // library, often include lwjgl in their class transformations, which causes errors with old ASM versions. + if(Integer.parseInt(version[0]) >= 5) continue; + Log.d(APP_NAME, "Library " + libItem.name + " has been changed to version 5.0.4"); + createLibraryInfo(libItem); + libItem.name = "org.ow2.asm:asm-all:5.0.4"; + libItem.url = null; + libItem.downloads.artifact.path = "org/ow2/asm/asm-all/5.0.4/asm-all-5.0.4.jar"; + libItem.downloads.artifact.sha1 = "e6244859997b3d4237a552669279780876228909"; + libItem.downloads.artifact.url = "https://repo1.maven.org/maven2/org/ow2/asm/asm-all/5.0.4/asm-all-5.0.4.jar"; } } } + private static void createLibraryInfo(DependentLibrary library) { + if(library.downloads == null || library.downloads.artifact == null) + library.downloads = new DependentLibrary.LibraryDownloads(new MinecraftLibraryArtifact()); + } + public static String[] generateLibClasspath(JMinecraftVersionList.Version info) { List libDir = new ArrayList<>(); for (DependentLibrary libItem: info.libraries) { @@ -1028,4 +1068,12 @@ public static void shareLog(Context context){ Intent sendIntent = Intent.createChooser(shareIntent, "latestlog.txt"); context.startActivity(sendIntent); } + + /** Mesure the textview height, given its current parameters */ + public static int mesureTextviewHeight(TextView t) { + int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(t.getWidth(), View.MeasureSpec.AT_MOST); + int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + t.measure(widthMeasureSpec, heightMeasureSpec); + return t.getMeasuredHeight(); + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java new file mode 100644 index 000000000..7b17f5cc0 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java @@ -0,0 +1,75 @@ +package net.kdt.pojavlaunch.contextexecutor; + +import android.app.Activity; +import android.app.Application; + +import net.kdt.pojavlaunch.Tools; + +import java.lang.ref.WeakReference; + +public class ContextExecutor { + private static WeakReference sApplication; + private static WeakReference sActivity; + + + /** + * Schedules a ContextExecutorTask to be executed. For more info on tasks, please read + * ContextExecutorTask.java + * @param contextExecutorTask the task to be executed + */ + public static void execute(ContextExecutorTask contextExecutorTask) { + Tools.runOnUiThread(()->executeOnUiThread(contextExecutorTask)); + } + + private static void executeOnUiThread(ContextExecutorTask contextExecutorTask) { + Activity activity = getWeakReference(sActivity); + if(activity != null) { + contextExecutorTask.executeWithActivity(activity); + return; + } + Application application = getWeakReference(sApplication); + if(application != null) { + contextExecutorTask.executeWithApplication(application); + }else { + throw new RuntimeException("ContextExecutor.execute() called before Application.onCreate!"); + } + } + + /** + * Set the Activity that this ContextExecutor will use for executing tasks + * @param activity the activity to be used + */ + public static void setActivity(Activity activity) { + sActivity = new WeakReference<>(activity); + } + + /** + * Clear the Activity previously set, so thet ContextExecutor won't use it to execute tasks. + */ + public static void clearActivity() { + if(sActivity != null) + sActivity.clear(); + } + + /** + * Set the Application that will be used to execute tasks if the Activity won't be available. + * @param application the application to use as the fallback + */ + public static void setApplication(Application application) { + sApplication = new WeakReference<>(application); + } + + /** + * Clear the Application previously set, so that ContextExecutor will notify the user of a critical error + * that is executing code after the application is ended by the system. + */ + public static void clearApplication() { + if(sApplication != null) + sApplication.clear(); + } + + private static T getWeakReference(WeakReference weakReference) { + if(weakReference == null) return null; + return weakReference.get(); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java new file mode 100644 index 000000000..9d8b1d3c3 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java @@ -0,0 +1,25 @@ +package net.kdt.pojavlaunch.contextexecutor; + +import android.app.Activity; +import android.content.Context; + +/** + * A ContextExecutorTask is a task that can dynamically change its behaviour, based on the context + * used for its execution. This can be used to implement for ex. error/finish notifications from + * background threads that may live with the Service after the activity that started them died. + */ +public interface ContextExecutorTask { + /** + * ContextExecutor will execute this function first if a foreground Activity that was attached to the + * ContextExecutor is available. + * @param activity the activity + */ + void executeWithActivity(Activity activity); + + /** + * ContextExecutor will execute this function if a foreground Activity is not available, but the app + * is still running. + * @param context the application context + */ + void executeWithApplication(Context context); +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabricInstallFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabricInstallFragment.java index 4626a1df9..582ad4835 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabricInstallFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabricInstallFragment.java @@ -42,7 +42,6 @@ public class FabricInstallFragment extends Fragment implements AdapterView.OnIte private String mSelectedGameVersion; private boolean mSelectedSnapshot; private ProgressBar mProgressBar; - private File mDestinationDir; private Button mStartButton; private View mRetryView; public FabricInstallFragment() { @@ -52,7 +51,6 @@ public FabricInstallFragment() { @Override public void onAttach(@NonNull Context context) { super.onAttach(context); - this.mDestinationDir = new File(Tools.DIR_CACHE, "fabric-installer"); } @Override @@ -88,7 +86,7 @@ private void onClickStart(View v) { return; } sTaskProxy = new ModloaderListenerProxy(); - FabricDownloadTask fabricDownloadTask = new FabricDownloadTask(sTaskProxy, mDestinationDir); + FabricDownloadTask fabricDownloadTask = new FabricDownloadTask(sTaskProxy); sTaskProxy.attachListener(this); mStartButton.setEnabled(false); new Thread(fabricDownloadTask).start(); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ForgeInstallFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ForgeInstallFragment.java index fe609697f..9455a23b4 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ForgeInstallFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ForgeInstallFragment.java @@ -59,7 +59,7 @@ public ExpandableListAdapter createAdapter(List versionList, LayoutInfla @Override public Runnable createDownloadTask(Object selectedVersion, ModloaderListenerProxy listenerProxy) { - return new ForgeDownloadTask(listenerProxy, (String) selectedVersion, new File(Tools.DIR_CACHE, "forge-installer.jar")); + return new ForgeDownloadTask(listenerProxy, (String) selectedVersion); } @Override diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java index 29e5b4d33..701a7a76b 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java @@ -13,16 +13,21 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.kdt.mcgui.mcVersionSpinner; + import net.kdt.pojavlaunch.CustomControlsActivity; import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.extra.ExtraConstants; import net.kdt.pojavlaunch.extra.ExtraCore; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; +import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; public class MainMenuFragment extends Fragment { public static final String TAG = "MainMenuFragment"; + private mcVersionSpinner mVersionSpinner; + public MainMenuFragment(){ super(R.layout.fragment_launcher); } @@ -36,6 +41,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat ImageButton mEditProfileButton = view.findViewById(R.id.edit_profile_button); Button mPlayButton = view.findViewById(R.id.play_button); + mVersionSpinner = view.findViewById(R.id.mc_version_spinner); mNewsButton.setOnClickListener(v -> Tools.openURL(requireActivity(), Tools.URL_HOME)); mCustomControlButton.setOnClickListener(v -> startActivity(new Intent(requireContext(), CustomControlsActivity.class))); @@ -51,10 +57,17 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat mShareLogsButton.setOnClickListener((v) -> shareLog(requireContext())); mNewsButton.setOnLongClickListener((v)->{ - Tools.swapFragment(requireActivity(), FabricInstallFragment.class, FabricInstallFragment.TAG, true, null); + Tools.swapFragment(requireActivity(), SearchModFragment.class, SearchModFragment.TAG, true, null); return true; }); } + + @Override + public void onResume() { + super.onResume(); + mVersionSpinner.reloadProfiles(); + } + private void runInstallerWithConfirmation(boolean isCustomArgs) { if (ProgressKeeper.getTaskCount() == 0) Tools.installMod(requireActivity(), isCustomArgs); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/OptiFineInstallFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/OptiFineInstallFragment.java index a37704618..abe7c0b39 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/OptiFineInstallFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/OptiFineInstallFragment.java @@ -51,8 +51,7 @@ public ExpandableListAdapter createAdapter(OptiFineUtils.OptiFineVersions versio @Override public Runnable createDownloadTask(Object selectedVersion, ModloaderListenerProxy listenerProxy) { - return new OptiFineDownloadTask((OptiFineUtils.OptiFineVersion) selectedVersion, - new File(Tools.DIR_CACHE, "optifine-installer.jar"), listenerProxy); + return new OptiFineDownloadTask((OptiFineUtils.OptiFineVersion) selectedVersion, listenerProxy); } @Override diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java index fc3a334a9..63f6c8365 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java @@ -86,7 +86,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat mDeleteButton.setOnClickListener(v -> { if(LauncherProfiles.mainProfileJson.profiles.size() > 1){ LauncherProfiles.mainProfileJson.profiles.remove(mProfileKey); - LauncherProfiles.update(); + LauncherProfiles.write(); ExtraCore.setValue(ExtraConstants.REFRESH_VERSION_SPINNER, DELETED_PROFILE); } @@ -211,7 +211,7 @@ private void save(){ LauncherProfiles.mainProfileJson.profiles.put(mProfileKey, mTempProfile); - LauncherProfiles.update(); + LauncherProfiles.write(); ExtraCore.setValue(ExtraConstants.REFRESH_VERSION_SPINNER, mProfileKey); } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileTypeSelectFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileTypeSelectFragment.java index 2c29ffff5..6fe618442 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileTypeSelectFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileTypeSelectFragment.java @@ -31,5 +31,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat Tools.swapFragment(requireActivity(), FabricInstallFragment.class, FabricInstallFragment.TAG, false, null)); view.findViewById(R.id.modded_profile_forge).setOnClickListener((v)-> Tools.swapFragment(requireActivity(), ForgeInstallFragment.class, ForgeInstallFragment.TAG, false, null)); + view.findViewById(R.id.modded_profile_modpack).setOnClickListener((v)-> + Tools.swapFragment(requireActivity(), SearchModFragment.class, SearchModFragment.TAG, false, null)); } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java new file mode 100644 index 000000000..ace5d14af --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java @@ -0,0 +1,165 @@ +package net.kdt.pojavlaunch.fragments; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.math.MathUtils; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.modloaders.modpacks.ModItemAdapter; +import net.kdt.pojavlaunch.modloaders.modpacks.api.CommonApi; +import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; +import net.kdt.pojavlaunch.profiles.VersionSelectorDialog; +import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; + +public class SearchModFragment extends Fragment implements ModItemAdapter.SearchResultCallback { + + public static final String TAG = "SearchModFragment"; + private View mOverlay; + private float mOverlayTopCache; // Padding cache reduce resource lookup + + private final RecyclerView.OnScrollListener mOverlayPositionListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + mOverlay.setY(MathUtils.clamp(mOverlay.getY() - dy, -mOverlay.getHeight(), mOverlayTopCache)); + } + }; + + private EditText mSearchEditText; + private ImageButton mFilterButton; + private RecyclerView mRecyclerview; + private ModItemAdapter mModItemAdapter; + private ProgressBar mSearchProgressBar; + private TextView mStatusTextView; + private ColorStateList mDefaultTextColor; + + private ModpackApi modpackApi; + + private final SearchFilters mSearchFilters; + + public SearchModFragment(){ + super(R.layout.fragment_mod_search); + mSearchFilters = new SearchFilters(); + mSearchFilters.isModpack = true; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + modpackApi = new CommonApi(context.getString(R.string.curseforge_api_key)); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + // You can only access resources after attaching to current context + mModItemAdapter = new ModItemAdapter(getResources(), modpackApi, this); + ProgressKeeper.addTaskCountListener(mModItemAdapter); + mOverlayTopCache = getResources().getDimension(R.dimen.fragment_padding_medium); + + mOverlay = view.findViewById(R.id.search_mod_overlay); + mSearchEditText = view.findViewById(R.id.search_mod_edittext); + mSearchProgressBar = view.findViewById(R.id.search_mod_progressbar); + mRecyclerview = view.findViewById(R.id.search_mod_list); + mStatusTextView = view.findViewById(R.id.search_mod_status_text); + mFilterButton = view.findViewById(R.id.search_mod_filter); + + mDefaultTextColor = mStatusTextView.getTextColors(); + + mRecyclerview.setLayoutManager(new LinearLayoutManager(getContext())); + mRecyclerview.setAdapter(mModItemAdapter); + + mRecyclerview.addOnScrollListener(mOverlayPositionListener); + + mSearchEditText.setOnEditorActionListener((v, actionId, event) -> { + mSearchProgressBar.setVisibility(View.VISIBLE); + mSearchFilters.name = mSearchEditText.getText().toString(); + mModItemAdapter.performSearchQuery(mSearchFilters); + return true; + }); + + mOverlay.post(()->{ + int overlayHeight = mOverlay.getHeight(); + mRecyclerview.setPadding(mRecyclerview.getPaddingLeft(), + mRecyclerview.getPaddingTop() + overlayHeight, + mRecyclerview.getPaddingRight(), + mRecyclerview.getPaddingBottom()); + }); + mFilterButton.setOnClickListener(v -> displayFilterDialog()); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + ProgressKeeper.removeTaskCountListener(mModItemAdapter); + mRecyclerview.removeOnScrollListener(mOverlayPositionListener); + } + + @Override + public void onSearchFinished() { + mSearchProgressBar.setVisibility(View.GONE); + mStatusTextView.setVisibility(View.GONE); + } + + @Override + public void onSearchError(int error) { + mSearchProgressBar.setVisibility(View.GONE); + mStatusTextView.setVisibility(View.VISIBLE); + switch (error) { + case ERROR_INTERNAL: + mStatusTextView.setTextColor(Color.RED); + mStatusTextView.setText(R.string.search_modpack_error); + break; + case ERROR_NO_RESULTS: + mStatusTextView.setTextColor(mDefaultTextColor); + mStatusTextView.setText(R.string.search_modpack_no_result); + break; + } + } + + private void displayFilterDialog() { + AlertDialog dialog = new AlertDialog.Builder(requireContext()) + .setView(R.layout.dialog_mod_filters) + .create(); + + // setup the view behavior + dialog.setOnShowListener(dialogInterface -> { + TextView mSelectedVersion = dialog.findViewById(R.id.search_mod_selected_mc_version_textview); + Button mSelectVersionButton = dialog.findViewById(R.id.search_mod_mc_version_button); + Button mApplyButton = dialog.findViewById(R.id.search_mod_apply_filters); + + assert mSelectVersionButton != null; + assert mSelectedVersion != null; + assert mApplyButton != null; + + // Setup the expendable list behavior + mSelectVersionButton.setOnClickListener(v -> VersionSelectorDialog.open(v.getContext(), true, (id, snapshot)-> mSelectedVersion.setText(id))); + + // Apply visually all the current settings + mSelectedVersion.setText(mSearchFilters.mcVersion); + + // Apply the new settings + mApplyButton.setOnClickListener(v -> { + mSearchFilters.mcVersion = mSelectedVersion.getText().toString(); + dialogInterface.dismiss(); + }); + }); + + + dialog.show(); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricDownloadTask.java index 03ea723ad..d0a5cd876 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricDownloadTask.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricDownloadTask.java @@ -15,9 +15,9 @@ public class FabricDownloadTask implements Runnable, Tools.DownloaderFeedback{ private final File mDestinationFile; private final ModloaderDownloadListener mModloaderDownloadListener; - public FabricDownloadTask(ModloaderDownloadListener modloaderDownloadListener, File mDestinationDir) { + public FabricDownloadTask(ModloaderDownloadListener modloaderDownloadListener) { this.mModloaderDownloadListener = modloaderDownloadListener; - this.mDestinationDir = mDestinationDir; + this.mDestinationDir = new File(Tools.DIR_CACHE, "fabric-installer"); this.mDestinationFile = new File(mDestinationDir, "fabric-installer.jar"); } @@ -29,7 +29,7 @@ public void run() { }catch (IOException e) { mModloaderDownloadListener.onDownloadError(e); } - ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, -1, -1); + ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); } private boolean runCatching() throws IOException { diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java index 60dcd9b2d..81d7f1d1a 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java @@ -10,39 +10,79 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.List; public class ForgeDownloadTask implements Runnable, Tools.DownloaderFeedback { - private final String mForgeUrl; - private final String mForgeVersion; - private final File mDestinationFile; + private String mDownloadUrl; + private String mFullVersion; + private String mLoaderVersion; + private String mGameVersion; private final ModloaderDownloadListener mListener; - public ForgeDownloadTask(ModloaderDownloadListener listener, String forgeVersion, File destinationFile) { + public ForgeDownloadTask(ModloaderDownloadListener listener, String forgeVersion) { this.mListener = listener; - this.mForgeUrl = ForgeUtils.getInstallerUrl(forgeVersion); - this.mForgeVersion = forgeVersion; - this.mDestinationFile = destinationFile; + this.mDownloadUrl = ForgeUtils.getInstallerUrl(forgeVersion); + this.mFullVersion = forgeVersion; + } + + public ForgeDownloadTask(ModloaderDownloadListener listener, String gameVersion, String loaderVersion) { + this.mListener = listener; + this.mLoaderVersion = loaderVersion; + this.mGameVersion = gameVersion; } @Override public void run() { - ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_progress, mForgeVersion); + if(determineDownloadUrl()) { + downloadForge(); + } + ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); + } + + @Override + public void updateProgress(int curr, int max) { + int progress100 = (int)(((float)curr / (float)max)*100f); + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.forge_dl_progress, mFullVersion); + } + + private void downloadForge() { + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_progress, mFullVersion); try { + File destinationFile = new File(Tools.DIR_CACHE, "forge-installer.jar"); byte[] buffer = new byte[8192]; - DownloadUtils.downloadFileMonitored(mForgeUrl, mDestinationFile, buffer, this); - mListener.onDownloadFinished(mDestinationFile); - }catch (IOException e) { - if(e instanceof FileNotFoundException) { + DownloadUtils.downloadFileMonitored(mDownloadUrl, destinationFile, buffer, this); + mListener.onDownloadFinished(destinationFile); + }catch (FileNotFoundException e) { + mListener.onDataNotAvailable(); + } catch (IOException e) { + mListener.onDownloadError(e); + } + } + + public boolean determineDownloadUrl() { + if(mDownloadUrl != null && mFullVersion != null) return true; + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_searching); + try { + if(!findVersion()) { mListener.onDataNotAvailable(); - }else{ - mListener.onDownloadError(e); + return false; } + }catch (IOException e) { + mListener.onDownloadError(e); + return false; } - ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, -1, -1); + return true; } - @Override - public void updateProgress(int curr, int max) { - int progress100 = (int)(((float)curr / (float)max)*100f); - ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.forge_dl_progress, mForgeVersion); + public boolean findVersion() throws IOException { + List forgeVersions = ForgeUtils.downloadForgeVersions(); + if(forgeVersions == null) return false; + String versionStart = mGameVersion+"-"+mLoaderVersion; + for(String versionName : forgeVersions) { + if(!versionName.startsWith(versionStart)) continue; + mFullVersion = versionName; + mDownloadUrl = ForgeUtils.getInstallerUrl(mFullVersion); + return true; + } + return false; } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeUtils.java index 31420de90..5925d9b4d 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeUtils.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeUtils.java @@ -59,4 +59,10 @@ public static void addAutoInstallArgs(Intent intent, File modInstallerJar, boole " -jar "+modInstallerJar.getAbsolutePath()); intent.putExtra("skipDetectMod", true); } + public static void addAutoInstallArgs(Intent intent, File modInstallerJar, String modpackFixupId) { + intent.putExtra("javaArgs", "-javaagent:"+ Tools.DIR_DATA+"/forge_installer/forge_installer.jar" + + "=\"" + modpackFixupId +"\"" + + " -jar "+modInstallerJar.getAbsolutePath()); + intent.putExtra("skipDetectMod", true); + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/OptiFineDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/OptiFineDownloadTask.java index c5278340d..d4fcfab68 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/OptiFineDownloadTask.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/OptiFineDownloadTask.java @@ -22,9 +22,9 @@ public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback, private final Object mMinecraftDownloadLock = new Object(); private Throwable mDownloaderThrowable; - public OptiFineDownloadTask(OptiFineUtils.OptiFineVersion mOptiFineVersion, File mDestinationFile, ModloaderDownloadListener mListener) { + public OptiFineDownloadTask(OptiFineUtils.OptiFineVersion mOptiFineVersion, ModloaderDownloadListener mListener) { this.mOptiFineVersion = mOptiFineVersion; - this.mDestinationFile = mDestinationFile; + this.mDestinationFile = new File(Tools.DIR_CACHE, "optifine-installer.jar"); this.mListener = mListener; } @@ -36,7 +36,7 @@ public void run() { }catch (IOException e) { mListener.onDownloadError(e); } - ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, -1, -1); + ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); } public boolean runCatching() throws IOException { diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModItemAdapter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModItemAdapter.java new file mode 100644 index 000000000..5eab19ab0 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModItemAdapter.java @@ -0,0 +1,409 @@ +package net.kdt.pojavlaunch.modloaders.modpacks; + +import android.annotation.SuppressLint; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.graphics.drawable.RoundedBitmapDrawable; +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; +import androidx.recyclerview.widget.RecyclerView; + +import com.kdt.SimpleArrayAdapter; + +import net.kdt.pojavlaunch.PojavApplication; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi; +import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ImageReceiver; +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.SearchFilters; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult; +import net.kdt.pojavlaunch.progresskeeper.TaskCountListener; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.Future; + +public class ModItemAdapter extends RecyclerView.Adapter implements TaskCountListener { + private static final ModItem[] MOD_ITEMS_EMPTY = new ModItem[0]; + private static final int VIEW_TYPE_MOD_ITEM = 0; + private static final int VIEW_TYPE_LOADING = 1; + + /* Used when versions haven't loaded yet, default text to reduce layout shifting */ + private final SimpleArrayAdapter mLoadingAdapter = new SimpleArrayAdapter<>(Collections.singletonList("Loading")); + /* This my seem horribly inefficient but it is in fact the most efficient way without effectively writing a weak collection from scratch */ + private final Set mViewHolderSet = Collections.newSetFromMap(new WeakHashMap<>()); + private final ModIconCache mIconCache = new ModIconCache(); + private final SearchResultCallback mSearchResultCallback; + private ModItem[] mModItems; + private final ModpackApi mModpackApi; + + /* Cache for ever so slightly rounding the image for the corner not to stick out of the layout */ + private final float mCornerDimensionCache; + + private Future mTaskInProgress; + private SearchFilters mSearchFilters; + private SearchResult mCurrentResult; + private boolean mLastPage; + private boolean mTasksRunning; + + + public ModItemAdapter(Resources resources, ModpackApi api, SearchResultCallback callback) { + mCornerDimensionCache = resources.getDimension(R.dimen._1sdp) / 250; + mModpackApi = api; + mModItems = new ModItem[]{}; + mSearchResultCallback = callback; + } + + public void performSearchQuery(SearchFilters searchFilters) { + if(mTaskInProgress != null) { + mTaskInProgress.cancel(true); + mTaskInProgress = null; + } + this.mSearchFilters = searchFilters; + this.mLastPage = false; + mTaskInProgress = new SelfReferencingFuture(new SearchApiTask(mSearchFilters, null)) + .startOnExecutor(PojavApplication.sExecutorService); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + LayoutInflater layoutInflater = LayoutInflater.from(viewGroup.getContext()); + View view; + switch (viewType) { + case VIEW_TYPE_MOD_ITEM: + // Create a new view, which defines the UI of the list item + view = layoutInflater.inflate(R.layout.view_mod, viewGroup, false); + return new ViewHolder(view); + case VIEW_TYPE_LOADING: + // Create a new view, which is actually just the progress bar + view = layoutInflater.inflate(R.layout.view_loading, viewGroup, false); + return new LoadingViewHolder(view); + default: + throw new RuntimeException("Unimplemented view type!"); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + switch (getItemViewType(position)) { + case VIEW_TYPE_MOD_ITEM: + ((ModItemAdapter.ViewHolder)holder).setStateLimited(mModItems[position]); + break; + case VIEW_TYPE_LOADING: + loadMoreResults(); + break; + default: + throw new RuntimeException("Unimplemented view type!"); + } + } + + @Override + public int getItemCount() { + if(mLastPage || mModItems.length == 0) return mModItems.length; + return mModItems.length+1; + } + + private void loadMoreResults() { + if(mTaskInProgress != null) return; + mTaskInProgress = new SelfReferencingFuture(new SearchApiTask(mSearchFilters, mCurrentResult)) + .startOnExecutor(PojavApplication.sExecutorService); + } + + @Override + public int getItemViewType(int position) { + if(position < mModItems.length) return VIEW_TYPE_MOD_ITEM; + return VIEW_TYPE_LOADING; + } + + @Override + public void onUpdateTaskCount(int taskCount) { + Tools.runOnUiThread(()->{ + mTasksRunning = taskCount != 0; + for(ViewHolder viewHolder : mViewHolderSet) { + viewHolder.updateInstallButtonState(); + } + }); + } + + + /** + * Basic viewholder with expension capabilities + */ + public class ViewHolder extends RecyclerView.ViewHolder { + + private ModDetail mModDetail = null; + private ModItem mModItem = null; + private final TextView mTitle, mDescription; + private final ImageView mIconView, mSourceView; + private View mExtendedLayout; + private Spinner mExtendedSpinner; + private Button mExtendedButton; + private TextView mExtendedErrorTextView; + private Future mExtensionFuture; + private Bitmap mThumbnailBitmap; + private ImageReceiver mImageReceiver; + private boolean mInstallEnabled; + + /* Used to display available versions of the mod(pack) */ + private final SimpleArrayAdapter mVersionAdapter = new SimpleArrayAdapter<>(null); + + public ViewHolder(View view) { + super(view); + mViewHolderSet.add(this); + view.setOnClickListener(v -> { + if(!hasExtended()){ + // Inflate the ViewStub + mExtendedLayout = ((ViewStub)v.findViewById(R.id.mod_limited_state_stub)).inflate(); + mExtendedButton = mExtendedLayout.findViewById(R.id.mod_extended_select_version_button); + mExtendedSpinner = mExtendedLayout.findViewById(R.id.mod_extended_version_spinner); + mExtendedErrorTextView = mExtendedLayout.findViewById(R.id.mod_extended_error_textview); + + mExtendedButton.setOnClickListener(v1 -> mModpackApi.handleInstallation( + mExtendedButton.getContext().getApplicationContext(), + mModDetail, + mExtendedSpinner.getSelectedItemPosition())); + mExtendedSpinner.setAdapter(mLoadingAdapter); + } else { + if(isExtended()) closeDetailedView(); + else openDetailedView(); + } + + if(isExtended() && mModDetail == null && mExtensionFuture == null) { // only reload if no reloads are in progress + setDetailedStateDefault(); + /* + * Why do we do this? + * The reason is simple: multithreading is difficult as hell to manage + * Let me explain: + */ + mExtensionFuture = new SelfReferencingFuture(myFuture -> { + /* + * While we are sitting in the function below doing networking, the view might have already gotten recycled. + * If we didn't use a Future, we would have extended a ViewHolder with completely unrelated content + * or with an error that has never actually happened + */ + mModDetail = mModpackApi.getModDetails(mModItem); + System.out.println(mModDetail); + Tools.runOnUiThread(() -> { + /* + * Once we enter here, the state we're in is already defined - no view shuffling can happen on the UI + * thread while we are on the UI thread ourselves. If we were cancelled, this means that the future + * we were supposed to have no longer makes sense, so we return and do not alter the state (since we might + * alter the state of an unrelated item otherwise) + */ + if(myFuture.isCancelled()) return; + /* + * We do not null the future before returning since this field might already belong to a different item with its + * own Future, which we don't want to interfere with. + * But if the future is not cancelled, it is the right one for this ViewHolder, and we don't need it anymore, so + * let's help GC clean it up once we exit! + */ + mExtensionFuture = null; + setStateDetailed(mModDetail); + }); + }).startOnExecutor(PojavApplication.sExecutorService); + } + }); + + // Define click listener for the ViewHolder's View + mTitle = view.findViewById(R.id.mod_title_textview); + mDescription = view.findViewById(R.id.mod_body_textview); + mIconView = view.findViewById(R.id.mod_thumbnail_imageview); + mSourceView = view.findViewById(R.id.mod_source_imageview); + } + + /** Display basic info about the moditem */ + public void setStateLimited(ModItem item) { + mModDetail = null; + if(mThumbnailBitmap != null) { + mIconView.setImageBitmap(null); + mThumbnailBitmap.recycle(); + } + if(mImageReceiver != null) { + mIconCache.cancelImage(mImageReceiver); + } + if(mExtensionFuture != null) { + /* + * Since this method reinitializes the ViewHolder for a new mod, this Future stops being ours, so we cancel it + * and null it. The rest is handled above + */ + mExtensionFuture.cancel(true); + mExtensionFuture = null; + } + + mModItem = item; + // here the previous reference to the image receiver will disappear + mImageReceiver = bm->{ + mImageReceiver = null; + mThumbnailBitmap = bm; + RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(mIconView.getResources(), bm); + drawable.setCornerRadius(mCornerDimensionCache * bm.getHeight()); + mIconView.setImageDrawable(drawable); + }; + mIconCache.getImage(mImageReceiver, mModItem.getIconCacheTag(), mModItem.imageUrl); + mSourceView.setImageResource(getSourceDrawable(item.apiSource)); + mTitle.setText(item.title); + mDescription.setText(item.description); + + if(hasExtended()){ + closeDetailedView(); + } + } + + /** Display extended info/interaction about a modpack */ + private void setStateDetailed(ModDetail detailedItem) { + if(detailedItem != null) { + setInstallEnabled(true); + mExtendedErrorTextView.setVisibility(View.GONE); + mVersionAdapter.setObjects(Arrays.asList(detailedItem.versionNames)); + mExtendedSpinner.setAdapter(mVersionAdapter); + } else { + closeDetailedView(); + setInstallEnabled(false); + mExtendedErrorTextView.setVisibility(View.VISIBLE); + mExtendedSpinner.setAdapter(null); + mVersionAdapter.setObjects(null); + } + } + + private void openDetailedView() { + mExtendedLayout.setVisibility(View.VISIBLE); + mDescription.setMaxLines(99); + + // We need to align to the longer section + int futureBottom = mDescription.getBottom() + Tools.mesureTextviewHeight(mDescription) - mDescription.getHeight(); + ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) mExtendedLayout.getLayoutParams(); + params.topToBottom = futureBottom > mIconView.getBottom() ? R.id.mod_body_textview : R.id.mod_thumbnail_imageview; + mExtendedLayout.setLayoutParams(params); + } + + private void closeDetailedView(){ + mExtendedLayout.setVisibility(View.GONE); + mDescription.setMaxLines(3); + } + + private void setDetailedStateDefault() { + setInstallEnabled(false); + mExtendedSpinner.setAdapter(mLoadingAdapter); + mExtendedErrorTextView.setVisibility(View.GONE); + openDetailedView(); + } + + private boolean hasExtended(){ + return mExtendedLayout != null; + } + + private boolean isExtended(){ + return hasExtended() && mExtendedLayout.getVisibility() == View.VISIBLE; + } + + private int getSourceDrawable(int apiSource) { + switch (apiSource) { + case Constants.SOURCE_CURSEFORGE: + return R.drawable.ic_curseforge; + case Constants.SOURCE_MODRINTH: + return R.drawable.ic_modrinth; + default: + throw new RuntimeException("Unknown API source"); + } + } + + private void setInstallEnabled(boolean enabled) { + mInstallEnabled = enabled; + updateInstallButtonState(); + } + + private void updateInstallButtonState() { + if(mExtendedButton != null) + mExtendedButton.setEnabled(mInstallEnabled && !mTasksRunning); + } + } + + /** + * The view holder used to hold the progress bar at the end of the list + */ + private static class LoadingViewHolder extends RecyclerView.ViewHolder { + public LoadingViewHolder(View view) { + super(view); + } + } + + private class SearchApiTask implements SelfReferencingFuture.FutureInterface { + private final SearchFilters mSearchFilters; + private final SearchResult mPreviousResult; + + private SearchApiTask(SearchFilters searchFilters, SearchResult previousResult) { + this.mSearchFilters = searchFilters; + this.mPreviousResult = previousResult; + } + + @SuppressLint("NotifyDataSetChanged") + @Override + public void run(Future myFuture) { + SearchResult result = mModpackApi.searchMod(mSearchFilters, mPreviousResult); + ModItem[] resultModItems = result != null ? result.results : null; + if(resultModItems != null && resultModItems.length != 0 && mPreviousResult != null) { + ModItem[] newModItems = new ModItem[resultModItems.length + mModItems.length]; + System.arraycopy(mModItems, 0, newModItems, 0, mModItems.length); + System.arraycopy(resultModItems, 0, newModItems, mModItems.length, resultModItems.length); + resultModItems = newModItems; + } + ModItem[] finalModItems = resultModItems; + Tools.runOnUiThread(() -> { + if(myFuture.isCancelled()) return; + mTaskInProgress = null; + if(finalModItems == null) { + mSearchResultCallback.onSearchError(SearchResultCallback.ERROR_INTERNAL); + }else if(finalModItems.length == 0) { + if(mPreviousResult != null) { + mLastPage = true; + notifyItemChanged(mModItems.length); + mSearchResultCallback.onSearchFinished(); + return; + } + mSearchResultCallback.onSearchError(SearchResultCallback.ERROR_NO_RESULTS); + }else{ + mSearchResultCallback.onSearchFinished(); + } + mCurrentResult = result; + if(finalModItems == null) { + mModItems = MOD_ITEMS_EMPTY; + notifyDataSetChanged(); + return; + } + if(mPreviousResult != null) { + int prevLength = mModItems.length; + mModItems = finalModItems; + notifyItemChanged(prevLength); + notifyItemRangeInserted(prevLength+1, mModItems.length); + }else { + mModItems = finalModItems; + notifyDataSetChanged(); + } + }); + } + } + + public interface SearchResultCallback { + int ERROR_INTERNAL = 0; + int ERROR_NO_RESULTS = 1; + void onSearchFinished(); + void onSearchError(int error); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModloaderInstallTracker.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModloaderInstallTracker.java new file mode 100644 index 000000000..8ec11e3fa --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModloaderInstallTracker.java @@ -0,0 +1,106 @@ +package net.kdt.pojavlaunch.modloaders.modpacks; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +import net.kdt.pojavlaunch.modloaders.modpacks.api.ModLoader; + +import java.io.File; + +/** + * This class is meant to track the availability of a modloader that is ready to be installed (as a result of modpack installation) + * It is needed because having all this logic spread over LauncherActivity would be clumsy, and I think that this is the best way to + * ensure that the modloader installer will run, even if the user does not receive the notification or something else happens + */ +public class ModloaderInstallTracker implements SharedPreferences.OnSharedPreferenceChangeListener { + private final SharedPreferences mSharedPreferences; + private final Activity mActivity; + + /** + * Create a ModInstallTracker object. This must be done in the Activity's onCreate method. + * @param activity the host activity + */ + public ModloaderInstallTracker(Activity activity) { + mActivity = activity; + mSharedPreferences = getPreferences(activity); + + } + + /** + * Attach the ModloaderInstallTracker to the current Activity. Must be done in the Activity's + * onResume method + */ + public void attach() { + mSharedPreferences.registerOnSharedPreferenceChangeListener(this); + runCheck(); + } + + /** + * Detach the ModloaderInstallTracker from the current Activity. Must be done in the Activity's + * onPause method + */ + public void detach() { + mSharedPreferences.unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String prefName) { + if(!"modLoaderAvailable".equals(prefName)) return; + runCheck(); + } + + @SuppressLint("ApplySharedPref") + private void runCheck() { + if(!mSharedPreferences.getBoolean("modLoaderAvailable", false)) return; + SharedPreferences.Editor editor = mSharedPreferences.edit().putBoolean("modLoaderAvailable", false); + if(!editor.commit()) editor.apply(); + ModLoader modLoader = deserializeModLoader(mSharedPreferences); + File modInstallFile = deserializeInstallFile(mSharedPreferences); + if(modLoader == null || modInstallFile == null) return; + startModInstallation(modLoader, modInstallFile); + } + + private void startModInstallation(ModLoader modLoader, File modInstallFile) { + Intent installIntent = modLoader.getInstallationIntent(mActivity, modInstallFile); + mActivity.startActivity(installIntent); + } + + private static SharedPreferences getPreferences(Context context) { + return context.getSharedPreferences("modloader_info", Context.MODE_PRIVATE); + } + + /** + * Store the data necessary to start a ModLoader installation for the tracker to start the installer + * sometime. + * @param context the Context + * @param modLoader the ModLoader to store + * @param modInstallFile the installer jar to store + */ + @SuppressLint("ApplySharedPref") + public static void saveModLoader(Context context, ModLoader modLoader, File modInstallFile) { + SharedPreferences.Editor editor = getPreferences(context).edit(); + editor.putInt("modLoaderType", modLoader.modLoaderType); + editor.putString("modLoaderVersion", modLoader.modLoaderVersion); + editor.putString("minecraftVersion", modLoader.minecraftVersion); + editor.putString("modInstallerJar", modInstallFile.getAbsolutePath()); + editor.putBoolean("modLoaderAvailable", true); + editor.commit(); + } + + private static ModLoader deserializeModLoader(SharedPreferences sharedPreferences) { + if(!sharedPreferences.contains("modLoaderType") || + !sharedPreferences.contains("modLoaderVersion") || + !sharedPreferences.contains("minecraftVersion")) return null; + return new ModLoader(sharedPreferences.getInt("modLoaderType", -1), + sharedPreferences.getString("modLoaderVersion", ""), + sharedPreferences.getString("minecraftVersion", "")); + } + + private static File deserializeInstallFile(SharedPreferences sharedPreferences) { + if(!sharedPreferences.contains("modInstallerJar")) return null; + return new File(sharedPreferences.getString("modInstallerJar", "")); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/SelfReferencingFuture.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/SelfReferencingFuture.java new file mode 100644 index 000000000..6f7d625af --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/SelfReferencingFuture.java @@ -0,0 +1,40 @@ +package net.kdt.pojavlaunch.modloaders.modpacks; + +import android.util.Log; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +public class SelfReferencingFuture { + private final Object mFutureLock = new Object(); + private final FutureInterface mFutureInterface; + private Future mMyFuture; + + public SelfReferencingFuture(FutureInterface futureInterface) { + this.mFutureInterface = futureInterface; + } + + public Future startOnExecutor(ExecutorService executorService) { + Future future = executorService.submit(this::run); + synchronized (mFutureLock) { + mMyFuture = future; + mFutureLock.notify(); + } + return future; + } + + private void run() { + try { + synchronized (mFutureLock) { + if (mMyFuture == null) mFutureLock.wait(); + } + mFutureInterface.run(mMyFuture); + }catch (InterruptedException e) { + Log.i("SelfReferencingFuture", "Interrupted while acquiring own Future"); + } + } + + public interface FutureInterface { + void run(Future myFuture); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ApiHandler.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ApiHandler.java new file mode 100644 index 000000000..4c03ecf2b --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ApiHandler.java @@ -0,0 +1,164 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import android.util.ArrayMap; +import android.util.Log; + +import com.google.gson.Gson; + +import net.kdt.pojavlaunch.Tools; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@SuppressWarnings("unused") +public class ApiHandler { + public final String baseUrl; + public final Map additionalHeaders; + + public ApiHandler(String url) { + baseUrl = url; + additionalHeaders = null; + } + + public ApiHandler(String url, String apiKey) { + baseUrl = url; + additionalHeaders = new ArrayMap<>(); + additionalHeaders.put("x-api-key", apiKey); + } + + public T get(String endpoint, Class tClass) { + return getFullUrl(additionalHeaders, baseUrl + "/" + endpoint, tClass); + } + + public T get(String endpoint, HashMap query, Class tClass) { + return getFullUrl(additionalHeaders, baseUrl + "/" + endpoint, query, tClass); + } + + public T post(String endpoint, T body, Class tClass) { + return postFullUrl(additionalHeaders, baseUrl + "/" + endpoint, body, tClass); + } + + public T post(String endpoint, HashMap query, T body, Class tClass) { + return postFullUrl(additionalHeaders, baseUrl + "/" + endpoint, query, body, tClass); + } + + //Make a get request and return the response as a raw string; + public static String getRaw(String url) { + return getRaw(null, url); + } + + public static String getRaw(Map headers, String url) { + Log.d("ApiHandler", url); + try { + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + addHeaders(conn, headers); + InputStream inputStream = conn.getInputStream(); + String data = Tools.read(inputStream); + Log.d(ApiHandler.class.toString(), data); + inputStream.close(); + conn.disconnect(); + return data; + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + public static String postRaw(String url, String body) { + return postRaw(null, url, body); + } + + public static String postRaw(Map headers, String url, String body) { + try { + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Accept", "application/json"); + addHeaders(conn, headers); + conn.setDoOutput(true); + + OutputStream outputStream = conn.getOutputStream(); + byte[] input = body.getBytes(StandardCharsets.UTF_8); + outputStream.write(input, 0, input.length); + outputStream.close(); + + InputStream inputStream = conn.getInputStream(); + String data = Tools.read(inputStream); + inputStream.close(); + + conn.disconnect(); + return data; + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + private static void addHeaders(HttpURLConnection connection, Map headers) { + if(headers != null) { + for(String key : headers.keySet()) + connection.addRequestProperty(key, headers.get(key)); + } + } + + private static String parseQueries(HashMap query) { + StringBuilder params = new StringBuilder("?"); + for (String param : query.keySet()) { + String value = Objects.toString(query.get(param)); + params.append(urlEncodeUTF8(param)) + .append("=") + .append(urlEncodeUTF8(value)) + .append("&"); + } + return params.substring(0, params.length() - 1); + } + + public static T getFullUrl(String url, Class tClass) { + return getFullUrl(null, url, tClass); + } + + public static T getFullUrl(String url, HashMap query, Class tClass) { + return getFullUrl(null, url, query, tClass); + } + + public static T postFullUrl(String url, T body, Class tClass) { + return postFullUrl(null, url, body, tClass); + } + + public static T postFullUrl(String url, HashMap query, T body, Class tClass) { + return postFullUrl(null, url, query, body, tClass); + } + + public static T getFullUrl(Map headers, String url, Class tClass) { + return new Gson().fromJson(getRaw(headers, url), tClass); + } + + public static T getFullUrl(Map headers, String url, HashMap query, Class tClass) { + return getFullUrl(headers, url + parseQueries(query), tClass); + } + + public static T postFullUrl(Map headers, String url, T body, Class tClass) { + return new Gson().fromJson(postRaw(headers, url, body.toString()), tClass); + } + + public static T postFullUrl(Map headers, String url, HashMap query, T body, Class tClass) { + return new Gson().fromJson(postRaw(headers, url + parseQueries(query), body.toString()), tClass); + } + + private static String urlEncodeUTF8(String input) { + try { + return URLEncoder.encode(input, "UTF-8"); + }catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 is required"); + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java new file mode 100644 index 000000000..d7aaef318 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java @@ -0,0 +1,187 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import androidx.annotation.NonNull; + +import net.kdt.pojavlaunch.PojavApplication; +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.SearchFilters; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; + +/** + * Group all apis under the same umbrella, as another layer of abstraction + */ +public class CommonApi implements ModpackApi { + + private final ModpackApi mCurseforgeApi; + private final ModpackApi mModrinthApi; + private final ModpackApi[] mModpackApis; + + public CommonApi(String curseforgeApiKey) { + mCurseforgeApi = new CurseforgeApi(curseforgeApiKey); + mModrinthApi = new ModrinthApi(); + mModpackApis = new ModpackApi[]{mModrinthApi, mCurseforgeApi}; + } + + @Override + public SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult) { + CommonApiSearchResult commonApiSearchResult = (CommonApiSearchResult) previousPageResult; + // If there are no previous page results, create a new array. Otherwise, use the one from the previous page + SearchResult[] results = commonApiSearchResult == null ? + new SearchResult[mModpackApis.length] : commonApiSearchResult.searchResults; + + int totalSize = 0; + int totalTotalSize = 0; + + Future[] futures = new Future[mModpackApis.length]; + for(int i = 0; i < mModpackApis.length; i++) { + // If there is an array and its length is zero, this means that we've exhausted the results for this + // search query and we don't need to actually do the search + if(results[i] != null && results[i].results.length == 0) continue; + // If the previous page result is not null (aka the arrays aren't fresh) + // and the previous result is null, it means that na error has occured on the previous + // page. We lost contingency anyway, so don't bother requesting. + if(previousPageResult != null && results[i] == null) continue; + futures[i] = PojavApplication.sExecutorService.submit(new ApiDownloadTask(i, searchFilters, + results[i])); + } + + if(Thread.interrupted()) { + cancelAllFutures(futures); + return null; + } + boolean hasSuccessful = false; + // Count up all the results + for(int i = 0; i < mModpackApis.length; i++) { + Future future = futures[i]; + if(future == null) continue; + try { + SearchResult searchResult = results[i] = (SearchResult) future.get(); + if(searchResult != null) hasSuccessful = true; + else continue; + totalSize += searchResult.results.length; + totalTotalSize += searchResult.totalResultCount; + }catch (Exception e) { + cancelAllFutures(futures); + e.printStackTrace(); + return null; + } + } + if(!hasSuccessful) { + return null; + } + // Then build an array with all the mods + ArrayList filteredResults = new ArrayList<>(results.length); + + // Sanitize returned values + for(SearchResult result : results) { + if(result == null) continue; + ModItem[] searchResults = result.results; + // If the length is zero, we don't need to perform needless copies + if(searchResults.length == 0) continue; + filteredResults.add(searchResults); + } + filteredResults.trimToSize(); + if(Thread.interrupted()) return null; + + ModItem[] concatenatedItems = buildFusedResponse(filteredResults); + if(Thread.interrupted()) return null; + // Recycle or create new search result + if(commonApiSearchResult == null) commonApiSearchResult = new CommonApiSearchResult(); + commonApiSearchResult.searchResults = results; + commonApiSearchResult.totalResultCount = totalTotalSize; + commonApiSearchResult.results = concatenatedItems; + return commonApiSearchResult; + } + + @Override + public ModDetail getModDetails(ModItem item) { + return getModpackApi(item.apiSource).getModDetails(item); + } + + @Override + public ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException { + return getModpackApi(modDetail.apiSource).installMod(modDetail, selectedVersion); + } + + private @NonNull ModpackApi getModpackApi(int apiSource) { + switch (apiSource) { + case Constants.SOURCE_MODRINTH: + return mModrinthApi; + case Constants.SOURCE_CURSEFORGE: + return mCurseforgeApi; + default: + throw new UnsupportedOperationException("Unknown API source: " + apiSource); + } + } + + /** Fuse the arrays in a way that's fair for every endpoint */ + private ModItem[] buildFusedResponse(List modMatrix){ + int totalSize = 0; + + // Calculate the total size of the merged array + for (ModItem[] array : modMatrix) { + totalSize += array.length; + } + + ModItem[] fusedItems = new ModItem[totalSize]; + + int mergedIndex = 0; + int maxLength = 0; + + // Find the maximum length of arrays + for (ModItem[] array : modMatrix) { + if (array.length > maxLength) { + maxLength = array.length; + } + } + + // Populate the merged array + for (int i = 0; i < maxLength; i++) { + for (ModItem[] matrix : modMatrix) { + if (i < matrix.length) { + fusedItems[mergedIndex] = matrix[i]; + mergedIndex++; + } + } + } + + return fusedItems; + } + + private void cancelAllFutures(Future[] futures) { + for(Future future : futures) { + if(future == null) continue; + future.cancel(true); + } + } + + private class ApiDownloadTask implements Callable { + private final int mModApi; + private final SearchFilters mSearchFilters; + private final SearchResult mPreviousPageResult; + + private ApiDownloadTask(int modApi, SearchFilters searchFilters, SearchResult previousPageResult) { + this.mModApi = modApi; + this.mSearchFilters = searchFilters; + this.mPreviousPageResult = previousPageResult; + } + + @Override + public SearchResult call() { + return mModpackApis[mModApi].searchMod(mSearchFilters, mPreviousPageResult); + } + } + + class CommonApiSearchResult extends SearchResult { + SearchResult[] searchResults = new SearchResult[mModpackApis.length]; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java new file mode 100644 index 000000000..0920509bf --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java @@ -0,0 +1,242 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import android.content.Context; +import android.util.Log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +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.models.Constants; +import net.kdt.pojavlaunch.modloaders.modpacks.models.CurseManifest; +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.progresskeeper.ProgressKeeper; +import net.kdt.pojavlaunch.utils.FileUtils; +import net.kdt.pojavlaunch.utils.ZipUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.regex.Pattern; +import java.util.zip.ZipFile; + +public class CurseforgeApi implements ModpackApi{ + private static final Pattern sMcVersionPattern = Pattern.compile("([0-9]+)\\.([0-9]+)\\.?([0-9]+)?"); + // Stolen from + // https://github.com/AnzhiZhang/CurseForgeModpackDownloader/blob/6cb3f428459f0cc8f444d16e54aea4cd1186fd7b/utils/requester.py#L93 + private static final int CURSEFORGE_MINECRAFT_GAME_ID = 432; + private static final int CURSEFORGE_MODPACK_CLASS_ID = 4471; + // https://api.curseforge.com/v1/categories?gameId=432 and search for "Mods" (case-sensitive) + private static final int CURSEFORGE_MOD_CLASS_ID = 6; + private static final int CURSEFORGE_SORT_RELEVANCY = 1; + private static final int CURSEFORGE_PAGINATION_SIZE = 50; + private static final int CURSEFORGE_PAGINATION_END_REACHED = -1; + private static final int CURSEFORGE_PAGINATION_ERROR = -2; + + private final ApiHandler mApiHandler; + public CurseforgeApi(String apiKey) { + mApiHandler = new ApiHandler("https://api.curseforge.com/v1", apiKey); + } + + @Override + public SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult) { + CurseforgeSearchResult curseforgeSearchResult = (CurseforgeSearchResult) previousPageResult; + + HashMap params = new HashMap<>(); + params.put("gameId", CURSEFORGE_MINECRAFT_GAME_ID); + params.put("classId", searchFilters.isModpack ? CURSEFORGE_MODPACK_CLASS_ID : CURSEFORGE_MOD_CLASS_ID); + params.put("searchFilter", searchFilters.name); + params.put("sortField", CURSEFORGE_SORT_RELEVANCY); + params.put("sortOrder", "desc"); + if(searchFilters.mcVersion != null && !searchFilters.mcVersion.isEmpty()) + params.put("gameVersion", searchFilters.mcVersion); + if(previousPageResult != null) + params.put("index", curseforgeSearchResult.previousOffset); + + JsonObject response = mApiHandler.get("mods/search", params, JsonObject.class); + if(response == null) return null; + JsonArray dataArray = response.getAsJsonArray("data"); + if(dataArray == null) return null; + JsonObject paginationInfo = response.getAsJsonObject("pagination"); + ArrayList modItemList = new ArrayList<>(dataArray.size()); + for(int i = 0; i < dataArray.size(); i++) { + JsonObject dataElement = dataArray.get(i).getAsJsonObject(); + JsonElement allowModDistribution = dataElement.get("allowModDistribution"); + // Gson automatically casts null to false, which leans to issues + // So, only check the distribution flag if it is non-null + if(!allowModDistribution.isJsonNull() && !allowModDistribution.getAsBoolean()) { + Log.i("CurseforgeApi", "Skipping modpack "+dataElement.get("name").getAsString() + " because curseforge sucks"); + continue; + } + ModItem modItem = new ModItem(Constants.SOURCE_CURSEFORGE, + searchFilters.isModpack, + dataElement.get("id").getAsString(), + dataElement.get("name").getAsString(), + dataElement.get("summary").getAsString(), + dataElement.getAsJsonObject("logo").get("thumbnailUrl").getAsString()); + modItemList.add(modItem); + } + if(curseforgeSearchResult == null) curseforgeSearchResult = new CurseforgeSearchResult(); + curseforgeSearchResult.results = modItemList.toArray(new ModItem[0]); + curseforgeSearchResult.totalResultCount = paginationInfo.get("totalCount").getAsInt(); + curseforgeSearchResult.previousOffset += dataArray.size(); + return curseforgeSearchResult; + + } + + @Override + public ModDetail getModDetails(ModItem item) { + ArrayList allModDetails = new ArrayList<>(); + int index = 0; + while(index != CURSEFORGE_PAGINATION_END_REACHED && + index != CURSEFORGE_PAGINATION_ERROR) { + index = getPaginatedDetails(allModDetails, index, item.id); + } + if(index == CURSEFORGE_PAGINATION_ERROR) return null; + int length = allModDetails.size(); + String[] versionNames = new String[length]; + String[] mcVersionNames = new String[length]; + String[] versionUrls = new String[length]; + for(int i = 0; i < allModDetails.size(); i++) { + JsonObject modDetail = allModDetails.get(i); + versionNames[i] = modDetail.get("displayName").getAsString(); + JsonElement downloadUrl = modDetail.get("downloadUrl"); + versionUrls[i] = downloadUrl.getAsString(); + JsonArray gameVersions = modDetail.getAsJsonArray("gameVersions"); + for(JsonElement jsonElement : gameVersions) { + String gameVersion = jsonElement.getAsString(); + if(!sMcVersionPattern.matcher(gameVersion).matches()) { + continue; + } + mcVersionNames[i] = gameVersion; + break; + } + } + return new ModDetail(item, versionNames, mcVersionNames, versionUrls); + } + + @Override + public ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException{ + //TODO considering only modpacks for now + return ModpackInstaller.installModpack(modDetail, selectedVersion, this::installCurseforgeZip); + } + + + private int getPaginatedDetails(ArrayList objectList, int index, String modId) { + HashMap params = new HashMap<>(); + params.put("index", index); + params.put("pageSize", CURSEFORGE_PAGINATION_SIZE); + + JsonObject response = mApiHandler.get("mods/"+modId+"/files", params, JsonObject.class); + if(response == null) return CURSEFORGE_PAGINATION_ERROR; + JsonArray data = response.getAsJsonArray("data"); + if(data == null) return CURSEFORGE_PAGINATION_ERROR; + for(int i = 0; i < data.size(); i++) { + JsonObject fileInfo = data.get(i).getAsJsonObject(); + if(fileInfo.get("isServerPack").getAsBoolean()) continue; + objectList.add(fileInfo); + } + if(data.size() < CURSEFORGE_PAGINATION_SIZE) { + return CURSEFORGE_PAGINATION_END_REACHED; // we read the remainder! yay! + } + return index + data.size(); + } + + private ModLoader installCurseforgeZip(File zipFile, File instanceDestination) throws IOException { + try (ZipFile modpackZipFile = new ZipFile(zipFile)){ + CurseManifest curseManifest = Tools.GLOBAL_GSON.fromJson( + Tools.read(ZipUtils.getEntryStream(modpackZipFile, "manifest.json")), + CurseManifest.class); + if(!verifyManifest(curseManifest)) { + Log.i("CurseforgeApi","manifest verification failed"); + return null; + } + ModDownloader modDownloader = new ModDownloader(new File(instanceDestination,"mods"), true); + int fileCount = curseManifest.files.length; + for(int i = 0; i < fileCount; i++) { + final CurseManifest.CurseFile curseFile = curseManifest.files[i]; + modDownloader.submitDownload(()->{ + String url = getDownloadUrl(curseFile.projectID, curseFile.fileID); + if(url == null && curseFile.required) + throw new IOException("Failed to obtain download URL for "+curseFile.projectID+" "+curseFile.fileID); + else if(url == null) return null; + return new ModDownloader.FileInfo(url, FileUtils.getFileName(url)); + }); + } + modDownloader.awaitFinish((c,m)-> + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, (int) Math.max((float)c/m*100,0), R.string.modpack_download_downloading_mods_fc, c, m) + ); + String overridesDir = "overrides"; + if(curseManifest.overrides != null) overridesDir = curseManifest.overrides; + ZipUtils.zipExtract(modpackZipFile, overridesDir, instanceDestination); + return createInfo(curseManifest.minecraft); + } + } + + private ModLoader createInfo(CurseManifest.CurseMinecraft minecraft) { + CurseManifest.CurseModLoader primaryModLoader = null; + for(CurseManifest.CurseModLoader modLoader : minecraft.modLoaders) { + if(modLoader.primary) { + primaryModLoader = modLoader; + break; + } + } + if(primaryModLoader == null) primaryModLoader = minecraft.modLoaders[0]; + String modLoaderId = primaryModLoader.id; + int dashIndex = modLoaderId.indexOf('-'); + String modLoaderName = modLoaderId.substring(0, dashIndex); + String modLoaderVersion = modLoaderId.substring(dashIndex+1); + Log.i("CurseforgeApi", modLoaderId + " " + modLoaderName + " "+modLoaderVersion); + int modLoaderTypeInt; + switch (modLoaderName) { + case "forge": + modLoaderTypeInt = ModLoader.MOD_LOADER_FORGE; + break; + case "fabric": + modLoaderTypeInt = ModLoader.MOD_LOADER_FABRIC; + break; + default: + return null; + //TODO: Quilt is also Forge? How does that work? + } + return new ModLoader(modLoaderTypeInt, modLoaderVersion, minecraft.version); + } + + private String getDownloadUrl(long projectID, long fileID) { + // First try the official api endpoint + JsonObject response = mApiHandler.get("mods/"+projectID+"/files/"+fileID+"/download-url", JsonObject.class); + if (response != null && !response.get("data").isJsonNull()) + return response.get("data").getAsString(); + + // Otherwise, fallback to building an edge link + JsonObject fallbackResponse = mApiHandler.get(String.format("mods/%s/files/%s", projectID, fileID), JsonObject.class); + if (fallbackResponse != null && !fallbackResponse.get("data").isJsonNull()){ + JsonObject modData = fallbackResponse.get("data").getAsJsonObject(); + int id = modData.get("id").getAsInt(); + return String.format("https://edge.forgecdn.net/files/%s/%s/%s", id/1000, id % 1000, modData.get("fileName").getAsString()); + } + + return null; + } + + private boolean verifyManifest(CurseManifest manifest) { + if(!"minecraftModpack".equals(manifest.manifestType)) return false; + if(manifest.manifestVersion != 1) return false; + if(manifest.minecraft == null) return false; + if(manifest.minecraft.version == null) return false; + if(manifest.minecraft.modLoaders == null) return false; + if(manifest.minecraft.modLoaders.length < 1) return false; + return true; + } + + class CurseforgeSearchResult extends SearchResult { + int previousOffset; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModDownloader.java new file mode 100644 index 000000000..09ddd7c6e --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModDownloader.java @@ -0,0 +1,172 @@ +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 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 final boolean mUseFileCount; + private IOException mFirstIOException; + private long mTotalSize; + + public ModDownloader(File destinationDirectory) { + this(destinationDirectory, false); + } + + public ModDownloader(File destinationDirectory, boolean useFileCount) { + this.mDownloadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); + this.mDestinationDirectory = destinationDirectory; + this.mUseFileCount = useFileCount; + } + + public void submitDownload(int fileSize, String relativePath, String... url) { + if(mUseFileCount) mTotalSize += 1; + else mTotalSize += fileSize; + mDownloadPool.execute(new DownloadTask(url, new File(mDestinationDirectory, relativePath))); + } + + public void submitDownload(FileInfoProvider infoProvider) { + if(!mUseFileCount) throw new RuntimeException("This method can only be used in a file-counting ModDownloader"); + mTotalSize += 1; + mDownloadPool.execute(new FileInfoQueryTask(infoProvider)); + } + + 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()) { + mDownloadPool.shutdownNow(); + 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; + } + + private void downloadFailed(IOException exception) { + mTerminator.set(true); + synchronized (mExceptionSyncPoint) { + if(mFirstIOException == null) { + mFirstIOException = exception; + mExceptionSyncPoint.notify(); + } + } + } + + class FileInfoQueryTask implements Runnable { + private final FileInfoProvider mFileInfoProvider; + public FileInfoQueryTask(FileInfoProvider fileInfoProvider) { + this.mFileInfoProvider = fileInfoProvider; + } + @Override + public void run() { + try { + FileInfo fileInfo = mFileInfoProvider.getFileInfo(); + if(fileInfo == null) return; + new DownloadTask(new String[]{fileInfo.url}, + new File(mDestinationDirectory, fileInfo.relativePath)).run(); + }catch (IOException e) { + downloadFailed(e); + } + } + } + + 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) { + downloadFailed(exception); + } + } + + private IOException tryDownload(String sourceUrl) throws InterruptedException { + IOException exception = null; + for (int i = 0; i < 5; i++) { + try { + DownloadUtils.downloadFileMonitored(sourceUrl, mDestination, getThreadLocalBuffer(), this); + if(mUseFileCount) mDownloadSize.addAndGet(1); + return null; + } catch (InterruptedIOException e) { + throw new InterruptedException(); + } catch (IOException e) { + e.printStackTrace(); + exception = e; + } + if(!mUseFileCount) { + mDownloadSize.addAndGet(-last); + last = 0; + } + } + return exception; + } + + @Override + public void updateProgress(int curr, int max) { + if(mUseFileCount) return; + mDownloadSize.addAndGet(curr - last); + last = curr; + } + } + + public static class FileInfo { + public final String url; + public final String relativePath; + + public FileInfo(String url, String relativePath) { + this.url = url; + this.relativePath = relativePath; + } + } + + public interface FileInfoProvider { + FileInfo getFileInfo() throws IOException; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoader.java new file mode 100644 index 000000000..c47cfd48a --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoader.java @@ -0,0 +1,87 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import android.content.Context; +import android.content.Intent; + +import net.kdt.pojavlaunch.JavaGUILauncherActivity; +import net.kdt.pojavlaunch.modloaders.FabricDownloadTask; +import net.kdt.pojavlaunch.modloaders.FabricUtils; +import net.kdt.pojavlaunch.modloaders.ForgeDownloadTask; +import net.kdt.pojavlaunch.modloaders.ForgeUtils; +import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener; + +import java.io.File; + +public class ModLoader { + 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 ModLoader(int modLoaderType, String modLoaderVersion, String minecraftVersion) { + this.modLoaderType = modLoaderType; + this.modLoaderVersion = modLoaderVersion; + this.minecraftVersion = minecraftVersion; + } + + /** + * Get the Version ID (the name of the mod loader in the versions/ folder) + * @return the Version ID as a string + */ + public String getVersionId() { + switch (modLoaderType) { + case MOD_LOADER_FORGE: + return minecraftVersion+"-forge-"+modLoaderVersion; + case MOD_LOADER_FABRIC: + return "fabric-loader-"+modLoaderVersion+"-"+minecraftVersion; + case MOD_LOADER_QUILT: + throw new RuntimeException("Quilt is not supported"); + default: + return null; + } + } + + /** + * Get the Runnable that needs to run in order to download the mod loader + * @param listener the listener that gets notified of the installation status + * @return the task Runnable that needs to be ran + */ + public Runnable getDownloadTask(ModloaderDownloadListener listener) { + switch (modLoaderType) { + case MOD_LOADER_FORGE: + return new ForgeDownloadTask(listener, minecraftVersion, modLoaderVersion); + case MOD_LOADER_FABRIC: + return new FabricDownloadTask(listener); + case MOD_LOADER_QUILT: + throw new RuntimeException("Quilt is not supported"); + default: + return null; + } + } + + /** + * Get the Intent to start the graphical installation of the mod loader. + * This method should only be ran after the download task of the specified mod loader finishes. + * @param context the package resolving Context (can be the base context) + * @param modInstallerJar the JAR file of the mod installer, provided by ModloaderDownloadListener after the installation + * finishes. + * @return the Intent which the launcher needs to start in order to install the mod loader + */ + public Intent getInstallationIntent(Context context, File modInstallerJar) { + Intent baseIntent = new Intent(context, JavaGUILauncherActivity.class); + switch (modLoaderType) { + case MOD_LOADER_FORGE: + ForgeUtils.addAutoInstallArgs(baseIntent, modInstallerJar, getVersionId()); + return baseIntent; + case MOD_LOADER_FABRIC: + FabricUtils.addAutoInstallArgs(baseIntent, modInstallerJar, minecraftVersion, modLoaderVersion, false, false); + return baseIntent; + case MOD_LOADER_QUILT: + throw new RuntimeException("Quilt is not supported"); + default: + return null; + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java new file mode 100644 index 000000000..141468af8 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java @@ -0,0 +1,73 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + + +import android.content.Context; + +import com.kdt.mcgui.ProgressLayout; + +import net.kdt.pojavlaunch.PojavApplication; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +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 java.io.IOException; + +/** + * + */ +public interface ModpackApi { + + /** + * @param searchFilters Filters + * @param previousPageResult The result from the previous page + * @return the list of mod items from specified offset + */ + SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult); + + /** + * @param searchFilters Filters + * @return A list of mod items + */ + default SearchResult searchMod(SearchFilters searchFilters) { + return searchMod(searchFilters, null); + } + + /** + * Fetch the mod details + * @param item The moditem that was selected + * @return Detailed data about a mod(pack) + */ + ModDetail getModDetails(ModItem item); + + /** + * Download and install the mod(pack) + * @param modDetail The mod detail data + * @param selectedVersion The selected version + */ + default void handleInstallation(Context context, ModDetail modDetail, int selectedVersion) { + // Doing this here since when starting installation, the progress does not start immediately + // which may lead to two concurrent installations (very bad) + ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.global_waiting); + PojavApplication.sExecutorService.execute(() -> { + try { + ModLoader loaderInfo = installMod(modDetail, selectedVersion); + if (loaderInfo == null) return; + loaderInfo.getDownloadTask(new NotificationDownloadListener(context, loaderInfo)).run(); + }catch (IOException e) { + Tools.showErrorRemote(context, R.string.modpack_install_download_failed, e); + } + }); + } + + /** + * Install the mod(pack). + * May require the download of additional files. + * May requires launching the installation of a modloader + * @param modDetail The mod detail data + * @param selectedVersion The selected version + */ + ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException; +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackInstaller.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackInstaller.java new file mode 100644 index 000000000..708754280 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackInstaller.java @@ -0,0 +1,63 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import com.kdt.mcgui.ProgressLayout; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener; +import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ModIconCache; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; +import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper; +import net.kdt.pojavlaunch.utils.DownloadUtils; +import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; +import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +public class ModpackInstaller { + + public static ModLoader installModpack(ModDetail modDetail, int selectedVersion, InstallFunction installFunction) throws IOException{ + String versionUrl = modDetail.versionUrls[selectedVersion]; + String modpackName = modDetail.title.toLowerCase(Locale.ROOT).trim().replace(" ", "_" ); + + // Build a new minecraft instance, folder first + + // Get the modpack file + File modpackFile = new File(Tools.DIR_CACHE, modpackName + ".cf"); // Cache File + ModLoader modLoaderInfo; + try { + byte[] downloadBuffer = new byte[8192]; + DownloadUtils.downloadFileMonitored(versionUrl, modpackFile, downloadBuffer, + new DownloaderProgressWrapper(R.string.modpack_download_downloading_metadata, + ProgressLayout.INSTALL_MODPACK)); + // Install the modpack + modLoaderInfo = installFunction.installModpack(modpackFile, new File(Tools.DIR_GAME_HOME, "custom_instances/"+modpackName)); + + } finally { + modpackFile.delete(); + ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); + } + if(modLoaderInfo == null) { + return null; + } + + // Create the instance + MinecraftProfile profile = new MinecraftProfile(); + profile.gameDir = "./custom_instances/" + modpackName; + profile.name = modDetail.title; + profile.lastVersionId = modLoaderInfo.getVersionId(); + profile.icon = ModIconCache.getBase64Image(modDetail.getIconCacheTag()); + + + LauncherProfiles.mainProfileJson.profiles.put(modpackName, profile); + LauncherProfiles.write(); + + return modLoaderInfo; + } + + interface InstallFunction { + ModLoader installModpack(File modpackFile, File instanceDestination) throws IOException; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java new file mode 100644 index 000000000..e8eded460 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java @@ -0,0 +1,139 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +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.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.SearchFilters; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult; +import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper; +import net.kdt.pojavlaunch.utils.ZipUtils; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipFile; + +public class ModrinthApi implements ModpackApi{ + private final ApiHandler mApiHandler; + public ModrinthApi(){ + mApiHandler = new ApiHandler("https://api.modrinth.com/v2"); + } + + @Override + public SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult) { + ModrinthSearchResult modrinthSearchResult = (ModrinthSearchResult) previousPageResult; + HashMap params = new HashMap<>(); + + // Build the facets filters + StringBuilder facetString = new StringBuilder(); + facetString.append("["); + facetString.append(String.format("[\"project_type:%s\"]", searchFilters.isModpack ? "modpack" : "mod")); + if(searchFilters.mcVersion != null && !searchFilters.mcVersion.isEmpty()) + facetString.append(String.format(",[\"versions:%s\"]", searchFilters.mcVersion)); + facetString.append("]"); + params.put("facets", facetString.toString()); + params.put("query", searchFilters.name.replace(' ', '+')); + params.put("limit", 50); + params.put("index", "relevance"); + if(modrinthSearchResult != null) + params.put("offset", modrinthSearchResult.previousOffset); + + JsonObject response = mApiHandler.get("search", params, JsonObject.class); + if(response == null) return null; + JsonArray responseHits = response.getAsJsonArray("hits"); + if(responseHits == null) return null; + + ModItem[] items = new ModItem[responseHits.size()]; + for(int i=0; i dependencies = modrinthIndex.dependencies; + String mcVersion = dependencies.get("minecraft"); + if(mcVersion == null) return null; + String modLoaderVersion; + if((modLoaderVersion = dependencies.get("forge")) != null) { + return new ModLoader(ModLoader.MOD_LOADER_FORGE, modLoaderVersion, mcVersion); + } + if((modLoaderVersion = dependencies.get("fabric-loader")) != null) { + return new ModLoader(ModLoader.MOD_LOADER_FABRIC, modLoaderVersion, mcVersion); + } + if((modLoaderVersion = dependencies.get("quilt-loader")) != null) { + return new ModLoader(ModLoader.MOD_LOADER_QUILT, modLoaderVersion, mcVersion); + } + return null; + } + + private ModLoader 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 createInfo(modrinthIndex); + } + } + + class ModrinthSearchResult extends SearchResult { + int previousOffset; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/NotificationDownloadListener.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/NotificationDownloadListener.java new file mode 100644 index 000000000..98f4be4e6 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/NotificationDownloadListener.java @@ -0,0 +1,67 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +import androidx.core.app.NotificationCompat; + +import net.kdt.pojavlaunch.LauncherActivity; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener; +import net.kdt.pojavlaunch.modloaders.modpacks.ModloaderInstallTracker; +import net.kdt.pojavlaunch.value.NotificationConstants; + +import java.io.File; + +public class NotificationDownloadListener implements ModloaderDownloadListener { + + private final NotificationCompat.Builder mNotificationBuilder; + private final NotificationManager mNotificationManager; + private final Context mContext; + private final ModLoader mModLoader; + + public NotificationDownloadListener(Context context, ModLoader modLoader) { + mModLoader = modLoader; + mContext = context.getApplicationContext(); + mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + mNotificationBuilder = new NotificationCompat.Builder(context, "channel_id") + .setContentTitle(context.getString(R.string.modpack_install_notification_title)) + .setSmallIcon(R.drawable.notif_icon); + } + + @Override + public void onDownloadFinished(File downloadedFile) { + ModloaderInstallTracker.saveModLoader(mContext, mModLoader, downloadedFile); + Intent mainActivityIntent = new Intent(mContext, LauncherActivity.class); + Tools.runOnUiThread(()->sendIntentNotification(mainActivityIntent, R.string.modpack_install_notification_success)); + } + + @Override + public void onDataNotAvailable() { + Tools.runOnUiThread(()->sendEmptyNotification(R.string.modpack_install_notification_data_not_available)); + } + + @Override + public void onDownloadError(Exception e) { + Tools.showErrorRemote(mContext, R.string.modpack_install_modloader_download_failed, e); + } + + private void sendIntentNotification(Intent intent, int contentText) { + PendingIntent pendingInstallIntent = + PendingIntent.getActivity(mContext, NotificationConstants.PENDINGINTENT_CODE_DOWNLOAD_SERVICE, + intent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0); + + mNotificationBuilder.setContentText(mContext.getText(contentText)); + mNotificationBuilder.setContentIntent(pendingInstallIntent); + mNotificationManager.notify(NotificationConstants.NOTIFICATION_ID_DOWNLOAD_LISTENER, mNotificationBuilder.build()); + } + + private void sendEmptyNotification(int contentText) { + mNotificationBuilder.setContentText(mContext.getText(contentText)); + mNotificationManager.notify(NotificationConstants.NOTIFICATION_ID_DOWNLOAD_LISTENER, mNotificationBuilder.build()); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/DownloadImageTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/DownloadImageTask.java new file mode 100644 index 000000000..9c9bdc942 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/DownloadImageTask.java @@ -0,0 +1,61 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.imagecache; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import net.kdt.pojavlaunch.utils.DownloadUtils; + +import java.io.FileOutputStream; +import java.io.IOException; + +class DownloadImageTask implements Runnable { + private static final float BITMAP_FINAL_DIMENSION = 256f; + private final ReadFromDiskTask mParentTask; + private int mRetryCount; + DownloadImageTask(ReadFromDiskTask parentTask) { + this.mParentTask = parentTask; + this.mRetryCount = 0; + } + + @Override + public void run() { + boolean wasSuccessful = false; + while(mRetryCount < 5 && !(wasSuccessful = runCatching())) { + mRetryCount++; + } + // restart the parent task to read the image and send it to the receiver + // if it wasn't cancelled. If it was, then we just die here + if(wasSuccessful && !mParentTask.taskCancelled()) + mParentTask.iconCache.cacheLoaderPool.execute(mParentTask); + } + + public boolean runCatching() { + try { + IconCacheJanitor.waitForJanitorToFinish(); + DownloadUtils.downloadFile(mParentTask.imageUrl, mParentTask.cacheFile); + Bitmap bitmap = BitmapFactory.decodeFile(mParentTask.cacheFile.getAbsolutePath()); + if(bitmap == null) return false; + int bitmapWidth = bitmap.getWidth(), bitmapHeight = bitmap.getHeight(); + if(bitmapWidth <= BITMAP_FINAL_DIMENSION && bitmapHeight <= BITMAP_FINAL_DIMENSION) { + bitmap.recycle(); + return true; + } + float imageRescaleRatio = Math.min(BITMAP_FINAL_DIMENSION/bitmapWidth, BITMAP_FINAL_DIMENSION/bitmapHeight); + Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, + (int)(bitmapWidth * imageRescaleRatio), + (int)(bitmapHeight * imageRescaleRatio), + true); + bitmap.recycle(); + if(resizedBitmap == bitmap) return true; + try (FileOutputStream fileOutputStream = new FileOutputStream(mParentTask.cacheFile)) { + resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 80, fileOutputStream); + } finally { + resizedBitmap.recycle(); + } + return true; + }catch (IOException e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/IconCacheJanitor.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/IconCacheJanitor.java new file mode 100644 index 000000000..f35fadeaa --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/IconCacheJanitor.java @@ -0,0 +1,86 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.imagecache; + +import android.util.Log; + +import net.kdt.pojavlaunch.PojavApplication; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** + * This image is intended to keep the mod icon cache tidy (aka under 100 megabytes) + */ +public class IconCacheJanitor implements Runnable{ + public static final long CACHE_SIZE_LIMIT = 104857600; // The cache size limit, 100 megabytes + public static final long CACHE_BRINGDOWN = 52428800; // The size to which the cache should be brought + // in case of an overflow, 50 mb + private static Future sJanitorFuture; + private static boolean sJanitorRan = false; + private IconCacheJanitor() { + // don't allow others to create this + } + @Override + public void run() { + File modIconCachePath = ModIconCache.getImageCachePath(); + if(!modIconCachePath.isDirectory() || !modIconCachePath.canRead()) return; + File[] modIconFiles = modIconCachePath.listFiles(); + if(modIconFiles == null) return; + ArrayList writableModIconFiles = new ArrayList<>(modIconFiles.length); + long directoryFileSize = 0; + for(File modIconFile : modIconFiles) { + if(!modIconFile.isFile() || !modIconFile.canRead()) continue; + directoryFileSize += modIconFile.length(); + if(!modIconFile.canWrite()) continue; + writableModIconFiles.add(modIconFile); + } + if(directoryFileSize < CACHE_SIZE_LIMIT) { + Log.i("IconCacheJanitor", "Skipping cleanup because there's not enough to clean up"); + return; + } + Arrays.sort(modIconFiles, + (x,y)-> Long.compare(y.lastModified(), x.lastModified()) + ); + int filesCleanedUp = 0; + for(File modFile : writableModIconFiles) { + if(directoryFileSize < CACHE_BRINGDOWN) break; + long modFileSize = modFile.length(); + if(modFile.delete()) { + directoryFileSize -= modFileSize; + filesCleanedUp++; + } + } + Log.i("IconCacheJanitor", "Cleaned up "+filesCleanedUp+ " files"); + synchronized (IconCacheJanitor.class) { + sJanitorFuture = null; + sJanitorRan = true; + } + } + + /** + * Runs the janitor task, unless there was one running already or one has ran already + */ + public static void runJanitor() { + synchronized (IconCacheJanitor.class) { + if (sJanitorFuture != null || sJanitorRan) return; + sJanitorFuture = PojavApplication.sExecutorService.submit(new IconCacheJanitor()); + } + } + + /** + * Waits for the janitor task to finish, if there is one running already + * Note that the thread waiting must not be interrupted. + */ + public static void waitForJanitorToFinish() { + synchronized (IconCacheJanitor.class) { + if (sJanitorFuture == null) return; + try { + sJanitorFuture.get(); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException("Should not happen!", e); + } + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ImageReceiver.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ImageReceiver.java new file mode 100644 index 000000000..f405b657c --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ImageReceiver.java @@ -0,0 +1,10 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.imagecache; + +import android.graphics.Bitmap; + +/** + * ModIconCache will call your view back when the image becomes available with this interface + */ +public interface ImageReceiver { + void onImageAvailable(Bitmap image); +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ModIconCache.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ModIconCache.java new file mode 100644 index 000000000..af1775633 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ModIconCache.java @@ -0,0 +1,109 @@ +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; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ModIconCache { + ThreadPoolExecutor cacheLoaderPool = new ThreadPoolExecutor(10, + 10, + 1000, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>()); + File cachePath; + private final List> mCancelledReceivers = new ArrayList<>(); + public ModIconCache() { + cachePath = getImageCachePath(); + if(!cachePath.exists() && !cachePath.isFile() && Tools.DIR_CACHE.canWrite()) { + if(!cachePath.mkdirs()) + throw new RuntimeException("Failed to create icon cache directory"); + } + + } + static File getImageCachePath() { + return new File(Tools.DIR_CACHE, "mod_icons"); + } + + /** + * Get an image for a mod with the associated tag and URL to download it in case if its not cached + * @param imageReceiver the receiver interface that would get called when the image loads + * @param imageTag the tag of the image to keep track of it + * @param imageUrl the URL of the image in case if it's not cached + */ + public void getImage(ImageReceiver imageReceiver, String imageTag, String imageUrl) { + cacheLoaderPool.execute(new ReadFromDiskTask(this, imageReceiver, imageTag, imageUrl)); + } + + /** + * Mark the image obtainment task requested with this receiver as "cancelled". This means that + * this receiver will not be called back and that some tasks related to this image may be + * prevented from happening or interrupted. + * @param imageReceiver the receiver to cancel + */ + public void cancelImage(ImageReceiver imageReceiver) { + synchronized (mCancelledReceivers) { + mCancelledReceivers.add(new WeakReference<>(imageReceiver)); + } + } + + boolean checkCancelled(ImageReceiver imageReceiver) { + boolean isCanceled = false; + synchronized (mCancelledReceivers) { + Iterator> iterator = mCancelledReceivers.iterator(); + while (iterator.hasNext()) { + WeakReference reference = iterator.next(); + if (reference.get() == null) { + iterator.remove(); + continue; + } + if(reference.get() == imageReceiver) { + isCanceled = true; + } + } + } + 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; + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ReadFromDiskTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ReadFromDiskTask.java new file mode 100644 index 000000000..89f3ed41c --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ReadFromDiskTask.java @@ -0,0 +1,55 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.imagecache; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import net.kdt.pojavlaunch.Tools; + +import java.io.File; + +public class ReadFromDiskTask implements Runnable { + final ModIconCache iconCache; + final ImageReceiver imageReceiver; + final File cacheFile; + final String imageUrl; + + ReadFromDiskTask(ModIconCache iconCache, ImageReceiver imageReceiver, String cacheTag, String imageUrl) { + this.iconCache = iconCache; + this.imageReceiver = imageReceiver; + this.cacheFile = new File(iconCache.cachePath, cacheTag+".ca"); + this.imageUrl = imageUrl; + } + + public void runDownloadTask() { + iconCache.cacheLoaderPool.execute(new DownloadImageTask(this)); + } + + @Override + public void run() { + if(cacheFile.isDirectory()) { + return; + } + if(cacheFile.canRead()) { + IconCacheJanitor.waitForJanitorToFinish(); + Bitmap bitmap = BitmapFactory.decodeFile(cacheFile.getAbsolutePath()); + if(bitmap != null) { + Tools.runOnUiThread(()->{ + if(taskCancelled()) { + bitmap.recycle(); // do not leak the bitmap if the task got cancelled right at the end + return; + } + imageReceiver.onImageAvailable(bitmap); + }); + return; + } + } + if(iconCache.cachePath.canWrite() && + !taskCancelled()) { // don't run the download task if the task got canceled + runDownloadTask(); + } + } + @SuppressWarnings("BooleanMethodAlwaysInverted") + public boolean taskCancelled() { + return iconCache.checkCancelled(imageReceiver); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/Constants.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/Constants.java new file mode 100644 index 000000000..b628a2ae6 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/Constants.java @@ -0,0 +1,16 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.models; + +public class Constants { + private Constants(){} + + /** Types of modpack apis */ + public static final int SOURCE_MODRINTH = 0x0; + public static final int SOURCE_CURSEFORGE = 0x1; + public static final int SOURCE_TECHNIC = 0x2; + + /** Modrinth api, file environments */ + public static final String MODRINTH_FILE_ENV_REQUIRED = "required"; + public static final String MODRINTH_FILE_ENV_OPTIONAL = "optional"; + public static final String MODRINTH_FILE_ENV_UNSUPPORTED = "unsupported"; + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/CurseManifest.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/CurseManifest.java new file mode 100644 index 000000000..f7b82a4ca --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/CurseManifest.java @@ -0,0 +1,25 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.models; + +public class CurseManifest { + public String name; + public String version; + public String author; + public String manifestType; + public int manifestVersion; + public CurseFile[] files; + public CurseMinecraft minecraft; + public String overrides; + public static class CurseFile { + public long projectID; + public long fileID; + public boolean required; + } + public static class CurseMinecraft { + public String version; + public CurseModLoader[] modLoaders; + } + public static class CurseModLoader { + public String id; + public boolean primary; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java new file mode 100644 index 000000000..12f9ec5e1 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java @@ -0,0 +1,41 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.models; + + +import androidx.annotation.NonNull; + +import java.util.Arrays; + +public class ModDetail extends ModItem { + /* A cheap way to map from the front facing name to the underlying id */ + public String[] versionNames; + public String [] mcVersionNames; + public String[] versionUrls; + public ModDetail(ModItem item, String[] versionNames, String[] mcVersionNames, String[] versionUrls) { + super(item.apiSource, item.isModpack, item.id, item.title, item.description, item.imageUrl); + this.versionNames = versionNames; + this.mcVersionNames = mcVersionNames; + this.versionUrls = versionUrls; + + // Add the mc version to the version model + for (int i=0; i dependencies; + + + public static class ModrinthIndexFile { + public String path; + public String[] downloads; + public int fileSize; + + public ModrinthIndexFileHashes hashes; + + @Nullable public ModrinthIndexFileEnv env; + + @NonNull + @Override + public String toString() { + return "ModrinthIndexFile{" + + "path='" + path + '\'' + + ", downloads=" + Arrays.toString(downloads) + + ", fileSize=" + fileSize + + ", hashes=" + hashes + + '}'; + } + + public static class ModrinthIndexFileHashes { + public String sha1; + public String sha512; + + @NonNull + @Override + public String toString() { + return "ModrinthIndexFileHashes{" + + "sha1='" + sha1 + '\'' + + ", sha512='" + sha512 + '\'' + + '}'; + } + } + + public static class ModrinthIndexFileEnv { + public String client; + public String server; + + @NonNull + @Override + public String toString() { + return "ModrinthIndexFileEnv{" + + "client='" + client + '\'' + + ", server='" + server + '\'' + + '}'; + } + } + } + + @NonNull + @Override + public String toString() { + return "ModrinthIndex{" + + "formatVersion=" + formatVersion + + ", game='" + game + '\'' + + ", versionId='" + versionId + '\'' + + ", name='" + name + '\'' + + ", summary='" + summary + '\'' + + ", files=" + Arrays.toString(files) + + '}'; + } + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchFilters.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchFilters.java new file mode 100644 index 000000000..5694b3b1e --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchFilters.java @@ -0,0 +1,13 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.models; + +import org.jetbrains.annotations.Nullable; + +/** + * Search filters, passed to APIs + */ +public class SearchFilters { + public boolean isModpack; + public String name; + @Nullable public String mcVersion; + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchResult.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchResult.java new file mode 100644 index 000000000..94638435c --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchResult.java @@ -0,0 +1,6 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.models; + +public class SearchResult { + public int totalResultCount; + public ModItem[] results; +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/ProfileAdapter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/ProfileAdapter.java index a3a9266da..91295d5b2 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/ProfileAdapter.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/ProfileAdapter.java @@ -31,15 +31,11 @@ public class ProfileAdapter extends BaseAdapter { private Map mProfiles; private final MinecraftProfile dummy = new MinecraftProfile(); private List mProfileList; - private final ProfileAdapterExtra[] mExtraEntires; + private ProfileAdapterExtra[] mExtraEntires; public ProfileAdapter(Context context, ProfileAdapterExtra[] extraEntries) { ProfileIconCache.initDefault(context); - LauncherProfiles.update(); - mProfiles = new HashMap<>(LauncherProfiles.mainProfileJson.profiles); - if(extraEntries == null) mExtraEntires = new ProfileAdapterExtra[0]; - else mExtraEntires = extraEntries; - mProfileList = new ArrayList<>(Arrays.asList(mProfiles.keySet().toArray(new String[0]))); + reloadProfiles(extraEntries); } /* * Gets how much profiles are loaded in the adapter right now @@ -67,6 +63,8 @@ public Object getItem(int position) { return null; } + + public int resolveProfileIndex(String name) { return mProfileList.indexOf(name); } @@ -134,4 +132,19 @@ public void setViewExtra(View v, ProfileAdapterExtra extra) { extendedTextView.setText(extra.name); extendedTextView.setBackgroundColor(Color.TRANSPARENT); } + + /** Reload profiles from the file */ + public void reloadProfiles(){ + LauncherProfiles.load(); + mProfiles = new HashMap<>(LauncherProfiles.mainProfileJson.profiles); + mProfileList = new ArrayList<>(Arrays.asList(mProfiles.keySet().toArray(new String[0]))); + notifyDataSetChanged(); + } + + /** Reload profiles from the file, with additional extra entries */ + public void reloadProfiles(ProfileAdapterExtra[] extraEntries) { + if(extraEntries == null) mExtraEntires = new ProfileAdapterExtra[0]; + else mExtraEntires = extraEntries; + this.reloadProfiles(); + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/progresskeeper/DownloaderProgressWrapper.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/progresskeeper/DownloaderProgressWrapper.java new file mode 100644 index 000000000..bf2818186 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/progresskeeper/DownloaderProgressWrapper.java @@ -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); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java index 00babeca1..a88416961 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java @@ -14,6 +14,7 @@ import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.value.NotificationConstants; import java.lang.ref.WeakReference; @@ -38,14 +39,15 @@ public int onStartCommand(Intent intent, int flags, int startId) { } Intent killIntent = new Intent(getApplicationContext(), GameService.class); killIntent.putExtra("kill", true); - PendingIntent pendingKillIntent = PendingIntent.getService(this, 0, killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0); + PendingIntent pendingKillIntent = PendingIntent.getService(this, NotificationConstants.PENDINGINTENT_CODE_KILL_GAME_SERVICE + , killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "channel_id") .setContentTitle(getString(R.string.lazy_service_default_title)) .setContentText(getString(R.string.notification_game_runs)) .addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_terminate), pendingKillIntent) .setSmallIcon(R.drawable.notif_icon) .setNotificationSilent(); - startForeground(2, notificationBuilder.build()); + startForeground(NotificationConstants.NOTIFICATION_ID_GAME_SERVICE, notificationBuilder.build()); return START_NOT_STICKY; // non-sticky so android wont try restarting the game after the user uses the "Quit" button } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java index b5fc6396b..2ffa71eb9 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java @@ -19,6 +19,7 @@ import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; import net.kdt.pojavlaunch.progresskeeper.TaskCountListener; +import net.kdt.pojavlaunch.value.NotificationConstants; /** * Lazy service which allows the process not to get killed. @@ -42,7 +43,8 @@ public void onCreate() { notificationManagerCompat = NotificationManagerCompat.from(getApplicationContext()); Intent killIntent = new Intent(getApplicationContext(), ProgressService.class); killIntent.putExtra("kill", true); - PendingIntent pendingKillIntent = PendingIntent.getService(this, 0, killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0); + PendingIntent pendingKillIntent = PendingIntent.getService(this, NotificationConstants.PENDINGINTENT_CODE_KILL_PROGRESS_SERVICE + , killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0); mNotificationBuilder = new NotificationCompat.Builder(this, "channel_id") .setContentTitle(getString(R.string.lazy_service_default_title)) .addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_terminate), pendingKillIntent) @@ -62,7 +64,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { } Log.d("ProgressService", "Started!"); mNotificationBuilder.setContentText(getString(R.string.progresslayout_tasks_in_progress, ProgressKeeper.getTaskCount())); - startForeground(1, mNotificationBuilder.build()); + startForeground(NotificationConstants.NOTIFICATION_ID_PROGRESS_SERVICE, mNotificationBuilder.build()); if(ProgressKeeper.getTaskCount() < 1) stopSelf(); else ProgressKeeper.addTaskCountListener(this, false); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java index 74112fefd..4de6935c3 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java @@ -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.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 */ diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/FileUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/FileUtils.java index 44cc8c5fb..2256c6cc5 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/FileUtils.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/FileUtils.java @@ -1,9 +1,20 @@ package net.kdt.pojavlaunch.utils; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; public class FileUtils { public static boolean exists(String filePath){ return new File(filePath).exists(); } + + public static String getFileName(String pathOrUrl) { + int lastSlashIndex = pathOrUrl.lastIndexOf('/'); + if(lastSlashIndex == -1) return null; + return pathOrUrl.substring(lastSlashIndex); + } } \ No newline at end of file diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/ZipUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/ZipUtils.java new file mode 100644 index 000000000..a56fd661b --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/ZipUtils.java @@ -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 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); + } + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/NotificationConstants.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/NotificationConstants.java new file mode 100644 index 000000000..02c23596e --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/NotificationConstants.java @@ -0,0 +1,12 @@ +package net.kdt.pojavlaunch.value; + +public class NotificationConstants { + public static final int NOTIFICATION_ID_PROGRESS_SERVICE = 1; + public static final int NOTIFICATION_ID_GAME_SERVICE = 2; + public static final int NOTIFICATION_ID_DOWNLOAD_LISTENER = 3; + public static final int NOTIFICATION_ID_SHOW_ERROR = 4; + public static final int PENDINGINTENT_CODE_KILL_PROGRESS_SERVICE = 1; + public static final int PENDINGINTENT_CODE_KILL_GAME_SERVICE = 2; + public static final int PENDINGINTENT_CODE_DOWNLOAD_SERVICE = 3; + public static final int PENDINGINTENT_CODE_SHOW_ERROR = 4; +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/launcherprofiles/LauncherProfiles.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/launcherprofiles/LauncherProfiles.java index 75af9c474..7ab6b407c 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/launcherprofiles/LauncherProfiles.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/launcherprofiles/LauncherProfiles.java @@ -8,45 +8,51 @@ import net.kdt.pojavlaunch.prefs.LauncherPreferences; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.UUID; public class LauncherProfiles { public static MinecraftLauncherProfiles mainProfileJson; - public static final File launcherProfilesFile = new File(Tools.DIR_GAME_NEW, "launcher_profiles.json"); - public static MinecraftLauncherProfiles update() { - try { - if (mainProfileJson == null) { - if (launcherProfilesFile.exists()) { - mainProfileJson = Tools.GLOBAL_GSON.fromJson(Tools.read(launcherProfilesFile.getAbsolutePath()), MinecraftLauncherProfiles.class); - if(mainProfileJson.profiles == null) mainProfileJson.profiles = new HashMap<>(); - else if(LauncherProfiles.normalizeProfileIds(mainProfileJson)){ - LauncherProfiles.update(); - } - } else { - mainProfileJson = new MinecraftLauncherProfiles(); - mainProfileJson.profiles = new HashMap<>(); - } + private static final File launcherProfilesFile = new File(Tools.DIR_GAME_NEW, "launcher_profiles.json"); - // Make sure we have a default profile on start - if (mainProfileJson.profiles.size() == 0){ - mainProfileJson.profiles.put("(Default)", MinecraftProfile.getDefaultProfile()); - LauncherProfiles.update(); - } - } else { - Tools.write(launcherProfilesFile.getAbsolutePath(), mainProfileJson.toJson()); + /** Reload the profile from the file, creating a default one if necessary */ + public static void load(){ + if (launcherProfilesFile.exists()) { + try { + mainProfileJson = Tools.GLOBAL_GSON.fromJson(Tools.read(launcherProfilesFile.getAbsolutePath()), MinecraftLauncherProfiles.class); + } catch (IOException e) { + Log.e(LauncherProfiles.class.toString(), "Failed to load file: ", e); + throw new RuntimeException(e); } + } - // insertMissing(); - return mainProfileJson; - } catch (Throwable th) { - throw new RuntimeException(th); + // Fill with default + if (mainProfileJson == null) mainProfileJson = new MinecraftLauncherProfiles(); + if (mainProfileJson.profiles == null) mainProfileJson.profiles = new HashMap<>(); + if (mainProfileJson.profiles.size() == 0) + mainProfileJson.profiles.put(UUID.randomUUID().toString(), MinecraftProfile.getDefaultProfile()); + + // Normalize profile names from mod installers + if(normalizeProfileIds(mainProfileJson)){ + write(); + load(); + } + } + + /** Apply the current configuration into a file */ + public static void write() { + try { + Tools.write(launcherProfilesFile.getAbsolutePath(), mainProfileJson.toJson()); + } catch (IOException e) { + Log.e(LauncherProfiles.class.toString(), "Failed to write profile file", e); + throw new RuntimeException(e); } } public static @NonNull MinecraftProfile getCurrentProfile() { - if(mainProfileJson == null) LauncherProfiles.update(); + if(mainProfileJson == null) LauncherProfiles.load(); String defaultProfileName = LauncherPreferences.DEFAULT_PREF.getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, ""); MinecraftProfile profile = mainProfileJson.profiles.get(defaultProfileName); if(profile == null) throw new RuntimeException("The current profile stopped existing :("); diff --git a/app_pojavlauncher/src/main/jni/ctxbridges/osmesa_loader.c b/app_pojavlauncher/src/main/jni/ctxbridges/osmesa_loader.c index c81c1977f..0edce435e 100644 --- a/app_pojavlauncher/src/main/jni/ctxbridges/osmesa_loader.c +++ b/app_pojavlauncher/src/main/jni/ctxbridges/osmesa_loader.c @@ -1,5 +1,5 @@ // -// Created by maks on 21.09.2022. +// Created by Vera-Firefly on 28.08.2023. // #include #include @@ -20,14 +20,11 @@ void (*glReadPixels_p) (GLint x, GLint y, GLsizei width, GLsizei height, GLenum void dlsym_OSMesa() { char* main_path = NULL; - char* alt_path = NULL; - if(asprintf(&main_path, "%s/libOSMesa_8.so", getenv("POJAV_NATIVEDIR")) == -1 || - asprintf(&alt_path, "%s/libOSMesa.so.8", getenv("POJAV_NATIVEDIR")) == -1) { + if(asprintf(&main_path, "%s/libOSMesa_8.so", getenv("POJAV_NATIVEDIR")) == -1) { abort(); } void* dl_handle = NULL; - dl_handle = dlopen(alt_path, RTLD_GLOBAL); - if(dl_handle == NULL) dl_handle = dlopen(main_path, RTLD_GLOBAL); + dl_handle = dlopen(main_path, RTLD_GLOBAL); if(dl_handle == NULL) abort(); OSMesaMakeCurrent_p = dlsym(dl_handle, "OSMesaMakeCurrent"); OSMesaGetCurrentContext_p = dlsym(dl_handle,"OSMesaGetCurrentContext"); @@ -39,4 +36,24 @@ void dlsym_OSMesa() { glClear_p = dlsym(dl_handle,"glClear"); glFinish_p = dlsym(dl_handle,"glFinish"); glReadPixels_p = dlsym(dl_handle,"glReadPixels"); -} \ No newline at end of file +} + +void dlsym_OSMesa_1() { + char* alt_path = NULL; + if(asprintf(&alt_path, "%s/libOSMesa_81.so", getenv("POJAV_NATIVEDIR")) == -1){ + abort(); + } + void* dl_handle = NULL; + dl_handle = dlopen(alt_path, RTLD_GLOBAL); + if(dl_handle == NULL) abort(); + OSMesaMakeCurrent_p = dlsym(dl_handle, "OSMesaMakeCurrent"); + OSMesaGetCurrentContext_p = dlsym(dl_handle,"OSMesaGetCurrentContext"); + OSMesaCreateContext_p = dlsym(dl_handle, "OSMesaCreateContext"); + OSMesaDestroyContext_p = dlsym(dl_handle, "OSMesaDestroyContext"); + OSMesaPixelStore_p = dlsym(dl_handle,"OSMesaPixelStore"); + glGetString_p = dlsym(dl_handle,"glGetString"); + glClearColor_p = dlsym(dl_handle, "glClearColor"); + glClear_p = dlsym(dl_handle,"glClear"); + glFinish_p = dlsym(dl_handle,"glFinish"); + glReadPixels_p = dlsym(dl_handle,"glReadPixels"); +} diff --git a/app_pojavlauncher/src/main/jni/ctxbridges/osmesa_loader.h b/app_pojavlauncher/src/main/jni/ctxbridges/osmesa_loader.h index 6cb76d25c..52eb4c8ea 100644 --- a/app_pojavlauncher/src/main/jni/ctxbridges/osmesa_loader.h +++ b/app_pojavlauncher/src/main/jni/ctxbridges/osmesa_loader.h @@ -1,5 +1,5 @@ // -// Created by maks on 21.09.2022. +// Created by Vera-Firefly on 28.08.2023. // #ifndef POJAVLAUNCHER_OSMESA_LOADER_H @@ -19,4 +19,5 @@ extern void (*glClearColor_p) (GLclampf red, GLclampf green, GLclampf blue, GLcl extern void (*glClear_p) (GLbitfield mask); extern void (*glReadPixels_p) (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void * data); void dlsym_OSMesa(); +void dlsym_OSMesa_1(); #endif //POJAVLAUNCHER_OSMESA_LOADER_H diff --git a/app_pojavlauncher/src/main/jni/egl_bridge.c b/app_pojavlauncher/src/main/jni/egl_bridge.c index 5707b5949..14c9fd443 100644 --- a/app_pojavlauncher/src/main/jni/egl_bridge.c +++ b/app_pojavlauncher/src/main/jni/egl_bridge.c @@ -700,9 +700,12 @@ void dlsym_OSMesa(void* dl_handle) { bool loadSymbols() { switch (pojav_environ->config_renderer) { case RENDERER_VIRGL: + dlsym_OSMesa_1(); dlsym_EGL(); + break; case RENDERER_VK_ZINK: dlsym_OSMesa(); + dlsym_EGL(); break; case RENDERER_GL4ES: //inside glbridge diff --git a/app_pojavlauncher/src/main/res/drawable/background_overlay.xml b/app_pojavlauncher/src/main/res/drawable/background_overlay.xml new file mode 100644 index 000000000..91493c05e --- /dev/null +++ b/app_pojavlauncher/src/main/res/drawable/background_overlay.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app_pojavlauncher/src/main/res/drawable/ic_curseforge.png b/app_pojavlauncher/src/main/res/drawable/ic_curseforge.png new file mode 100644 index 0000000000000000000000000000000000000000..2e19dc7107f0610c7a6a9b200031a9f19ff7a5ea GIT binary patch literal 5502 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 O5$qr?b6?=1jK2Xw;ZY_4 literal 0 HcmV?d00001 diff --git a/app_pojavlauncher/src/main/res/drawable/ic_filter.xml b/app_pojavlauncher/src/main/res/drawable/ic_filter.xml new file mode 100644 index 000000000..b03044595 --- /dev/null +++ b/app_pojavlauncher/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,9 @@ + + + diff --git a/app_pojavlauncher/src/main/res/drawable/ic_modrinth.png b/app_pojavlauncher/src/main/res/drawable/ic_modrinth.png new file mode 100644 index 0000000000000000000000000000000000000000..0d7b086592b5a5a68e7787dd662d76324f8ce136 GIT binary patch literal 6229 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 literal 0 HcmV?d00001 diff --git a/app_pojavlauncher/src/main/res/layout/dialog_mod_filters.xml b/app_pojavlauncher/src/main/res/layout/dialog_mod_filters.xml new file mode 100644 index 000000000..911f02b39 --- /dev/null +++ b/app_pojavlauncher/src/main/res/layout/dialog_mod_filters.xml @@ -0,0 +1,61 @@ + + + + + + + + +