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
zcmY*dWmFW5c)B&LM{(h7=Hxl#&nzq)WPUq(wqtfB~etyF*e?QfVoHk#3Ms
z>Y;bt_1-&w_P29==bS%#@9qPvstYV4O;t=RJPeG74>7E)DfAPFPO<(`WKv9
z${tD*Vqn~YFfce#wcG$kq%(j3VTkLaGn;%{-DDGOV=*ie*ZqbmY_2XAmnms(rAJWA
zm6`7C=tfg1!%00a&o1sZuDp#hs=$9Tb7niG%0t;`f(l!XdLm6(9B%Dsvkkcl5HhvN
z9;0GsJAO-lTi-{9?)fpmp={0Bx2{48uW)KvZnLWzVaJe_=i
zD__W}fJ5SfLB@>zb@7}|hkj(m6
z-Jy|>L#fb7FPmyfN@j9BuL;RSwf9E#-k|N{Bv@6BqOdNw!eeH5;fwC(Gq-_}2rcX`
z9_@0L5b^MxFkxivgznE61bAwIT78@9_yK)+f6johH1bP&>tb+)aDrM|py6Zr`z->&R-piT$1w^LUrDT!Wu-gCe
ztTfnB2fR&-uybhv3;BR+7;vDErd;_Wl*htVNfKGt2^J9MP8HmV!=T4Uf7fyf)#^%TWilGMzT;3pndqcNhL=qwnR6woUu_^>x$Y25%cQT@(pU!h>UaKL&+1T6KJHN_Q_>
z8o3l+(6K(v4c^dh9BLFDXaAllHThZ%ax{9)0Rgfrg&CPyCQQEC{fyR4sKG~B7(-4{
z@^T4zRm(MZSMf5~vEQw{SeaInDqfB_24B7)>_QZwgT5{M!DoD@6S1GGRAjNRsa5mG
z%&vLTnpC3!lFuYJzo6aTbX1R}bDVyufLRtso|Go_Zlo0I8b(Nw7G2PJcHla*Z+w>w
zTs$WWIVfup&DNkB)L{?3(d$s}Tkw<&^QO?EfQVaNzFHnZVuKU6b!HksqOXsuYpq}k
z+6eZ+BR=}W0sprWyGNfP{TFYD(Sc&+t2g4(jMG)twHOw?Vd*>d10ZB1Wx&;80vE%?
zu~|SEHu4RqbTKc7DuK4Lo8{^BLRLC?X+eE%%qm)3n(0iBuKADlmwpdy=}*+pHTqhq
z@$1`_DIDgDS5fmPF#2q224L&Ya`o-Y5JVjRnkoS<6A3;}iu(27TXaaJvY2flF^!$}
zhUCXQba`H5y2^uAhRHVxLZA>WO#`9cr$?_HvV`46TLYM7)1m_GL%*Kq#i{y)M21#u
z<XchsQhovwI>-3wIqiKz(rbm{8n0OJgDdgWl|pQnGZ_NIVE$5zSLMULdB<_X1HT
z4d$OS?Q!1m!Fw(Q8&V2a#jHFo#EuufXg;DR{4sqj6=0fsL(TQaZO5=?bxg-=m07#w
z0`2W_T@^p%9P?gv`znwg@rEHjiJV6W=QPDJj#|0Zx0SZ8p&W>K<|&U85nhj|KtDfX
zAroJfdpc9ar9UZZWf}R!2C0M**^ZDQZM%qOLvR-l$&l|VDWSk7X4lxa+I^L^OMEAX*
z!#@+?&;2FdvF*0D4_4+aw6#qT9!M2A_hL$yEZh;wL=L0vf{AAi?r@U+V3ZZmKBw~YkvFNggw6Y)h=h%5s(f)
zADKGd_-9>PohgG;VrQC1dRtJYVtdWJoa#Y{=Xj{|X`Wqsicy&oRlUk|635adKE+GC
zGFBPPFis0g)Js!Icej9EFt1Ti?WxUg!jaPOetR)kn#f*2N|8Fh^aKr29J5UU
z%Q2Taxr;MVpjOK0_}i>t?a9_nm>*ElD^Zr+H@+o&|hob9!f8Jv@)3-
za&qOkVn0wYP(l!%9|t*jIVm+|$78GgoqjdId)j)=f$5Q#bGp&a2#VlAbJK8<$n-dl
zZ;;3#10{f9@u688r?XN2KCo$2f{I10I~L2H+(`6W=V(AXi1&J7=v$@7p_h0gozYOn
z9vAF;zFdRun~(4pB`V^e0WV5Dup~$MN>{`K!d+@-5lMM5qM3@Rh@cnL4xy?mk5
z?RYRd#$xU>YULsHw0-xRPIs1XxxNmr3w~%F;dD##)44dsy_``AdN6#>2o9!~xYmr_
zTO3jz+u31NPf5+{jOjGaLkP?)dx5;`V4B@a_9ZuD5k7Z>Pccyfc><9pcCp-#$YxUg@@?D=GZ1TO|5nv!O35jU
z!8{$22IrM)!e?EOD?4@+mN}z7Znz_6_b#1G(C{-2>Kez7diZ
zsH}t`H&D={MA0)8z49$lNJ(`Mwo+FdBZM;~MxIdj_0VOb!v)RVe<Y$(~n2&
z?Q!sbY-L&bLHhck2$pnQ=00H$Feo29MM?We+m~N-f~cGQ>Xe{|Yw?X?USRqGdds9jf`
zJ&Cjl)04&@$C|K8;j%Bd4qY$HXGoC-iZuwZ~Twh0fq+zaX6}^c}whm
zoI;Q>?WOB?4Dynh%%4%ej1@oC7BznBMqk?LA2Hub(pC)qpggMoZNKno1><58(KAaj
zFiVy`>xv&Fg+GaDc^bOaPiJVm9O8ehw=pHpIxg`n#XFJbkT03uFGmEnP~YIB{b4(s
z801-BFZdu{kN5TOQvdny1DI2J<$7}T9vBuhSuAKv{l$FR3r+;3D6L6Nr=%|?$6Eu{
z3Tq3%U4ujHA*qJsc(ZVDE%4US&>K11Gg}0_wUAcOhiL^eP)n>$W>$LcSeS
zH0Yy-R1go@^h=ifbUs<-g2*o#(=>@SkXJi#3Tx7WyX5tR8Z9*lxd9dI@|yIZ_2`Co
zOS%OPJKb5f4R#|WeT~WC&=+T-$**`9Av0V)*fXkgsmXZ6rGDB%G>NoynrFcqzf#-Y
z5qUK~{?26vf0b3k_LN%HCHC3D16(@ps}EEu6+5nS1By)T%DvpfqZyXky}=G_Vvsb_
z6C+CFNEKXF9AOJ81YuFz*V`w6m91Q0b%_1cGfo+*C_)1u5EZEaTW#Q7{x-B0F$S5TW+IE8hxQ!4~sOk{$g3xBRrpa#qEK#e~hGI7B~Rx(hg%
z7$GuJ7yF*3*HY1yBT#57w}@!cGCxe%o!*%?>M0I%DeA!-;!#UfcVpQv|89$ZDP@)_
zhb%qud|#eNJ5q9P??c$3_3){ua?ym(N_gLP1Dj~W-cjY|d5(=?Pkv~xR}W^mGNe_M
z?ED;2%Jb$eIiO+qA@>%ELQ~!o4c5=Ng6{^f0z|=Ea&NV^D{jQ7;dX^AimW@kkj;``
zjL!%nDHcPP2@qH?;1tcIA|YZ#0M^g{=zuzRncFO|@NS)1$HkzBv?HbJadb=^>pU70
zS2%##=vi!dnR~*SebawiyYJksO6`k2ycaCpkwCwI0T2dh1coX1;jPIl^@Xp$}1U
zV6k9MIIT7k|L93}d^Fcv7N(qU5oiu-GnUIpf}J@V_24dF25>&vP?`8h5RzdfMB{Q$gs-XzP}rP}i$Y-4>1
zjuSfS-)0oou((F-c@=uqzq5q5%&Zsvq5WkG`&dLXw-4R2hJ^PPX((u3KmO|K!yioi
zPf5zJZV^N{%Uou|JX~8Z)2TkYx|j9@`?DDj3dP#`D3WEZdSKgNNpc8FDN8)QTW)I;
z2aL8;b_}xq@?nnrO7D{$wqqC_QYp2t&F%#)ZA0
z_9MzhtY>d5-dB=W_5&tEQD;dy)Nf+>5~u%M2M_U>V%k{l`NaCq?m<@LE*gG_tKqWP
z5pIgxux&`L%kJy1%(B_c*#7hNv~JW2X+bZ0Y>o^SbC;y^6gQim7I9pu919Fs1(@_u
zA1Op2sX3xEuye|vMsX;cP+iXi$a6jZWC3}sf5eu-{%w?+DWR4l^d|)Q`A>fwF1F3F
z74A7A6_z>Pmf}uSFh>M`$y^qp02)!;it4Wek=JT)i}Cv*48`^CUd32VSFj8B)JjHw
zRDmicrkjXTFl;Qw`e^seVks8xnUQJ8O~wl1@Evzl(#7>R)KopT*bfMmRhmvP@Og`>
z9NEv+5J)Be(Q5^I4)%GVzp<3_v{q=fN!AeoaN9GDcRitt_-z+QT!T4``HNc&5}-TT
zKAj)Z3lH`&h&?aydm2-z+%8CwySmez#~%aDO;t~oqD!$Mf2FRqI-z#t?*XSrGTMFr
z5Dbz(e<|x(7-bCf9(|v*^GtV@eBZv;!)Ra}0vgdDsPT*)BhRStPL39D{Zr=m)GiJ2
zILUD88#j@h%y@z_m2;o-
zy>MkrUmI{ZaReA~eB{6McsIa@OGZLbWthvu
zoR#Npl0@wuRE_A8hib0|v7x`R`W%egwddfOy~`Bmg)S>Gpie}5{UW=*QtG&f*`^8X
zBmV*lr`xK*2fDBDt8zyaRUB8~vwfdM)2jT}8}2voCLjYL>{i)N&=lr3YO6
zH29i-{&cIfs>6~QA*60jpC@
zf%mu!9hpwN5Ea|K@cau=&>IG}qCVCWEStlhpDrImwa1<8=Rdh`u}_>l7AkeD*e25J
z=1-eqwh!p;Ur_A4NcUOb${aAF*rf#jUfb6XK#nwh`JO^%%hf}^VSi}
zV(k%5cfn5A-rMYnGYZgY-UGM@&lxS&iqJvFg5Bm|9#>gQB*2FKmjNxi98ae
zL=6bt^P7|Uzx>4HP>ccqMIb-{S^b3eKM@P&|2IOy22~{hcm^c0LtP30WPwXQC<#G;
zE)X{Wl^_UE0?z-FRH!3C02Fu`^w&sl_^ahv|d>Frf1{B>gB0%2()~a`glfKF0Ky2siL<5?MJ;ha!aD+G&kOMxxM(qd#o&bq&Q9L5|
p@AH}#^;!g=1$1IUb%_8JFd2CPsBzBwSLGH3kUSvb1KfYk{{S2!M0NlG
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 extends ZipEntry> zipEntries = zipFile.entries();
+
+ int dirNameLen = dirName.length();
+ while(zipEntries.hasMoreElements()) {
+ ZipEntry zipEntry = zipEntries.nextElement();
+ String entryName = zipEntry.getName();
+ if(!entryName.startsWith(dirName) || zipEntry.isDirectory()) continue;
+ File zipDestination = new File(destination, entryName.substring(dirNameLen));
+ File parent = zipDestination.getParentFile();
+ if(parent != null && !parent.exists())
+ if(!parent.mkdirs()) throw new IOException("Failed to create "+parent.getAbsolutePath());
+ try (InputStream inputStream = zipFile.getInputStream(zipEntry);
+ OutputStream outputStream =
+ new FileOutputStream(zipDestination)) {
+ IOUtils.copy(inputStream, outputStream);
+ }
+ }
+ }
+}
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app_pojavlauncher/src/main/res/layout/fragment_mod_search.xml b/app_pojavlauncher/src/main/res/layout/fragment_mod_search.xml
new file mode 100644
index 000000000..292f308a7
--- /dev/null
+++ b/app_pojavlauncher/src/main/res/layout/fragment_mod_search.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app_pojavlauncher/src/main/res/layout/fragment_profile_type.xml b/app_pojavlauncher/src/main/res/layout/fragment_profile_type.xml
index 8624645c5..73bbdc292 100644
--- a/app_pojavlauncher/src/main/res/layout/fragment_profile_type.xml
+++ b/app_pojavlauncher/src/main/res/layout/fragment_profile_type.xml
@@ -103,6 +103,14 @@
app:layout_constraintTop_toBottomOf="@+id/modded_profile_fabric" />
+
+
+
+
+
\ No newline at end of file
diff --git a/app_pojavlauncher/src/main/res/layout/view_mod.xml b/app_pojavlauncher/src/main/res/layout/view_mod.xml
new file mode 100644
index 000000000..863ce17dc
--- /dev/null
+++ b/app_pojavlauncher/src/main/res/layout/view_mod.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app_pojavlauncher/src/main/res/layout/view_mod_extended.xml b/app_pojavlauncher/src/main/res/layout/view_mod_extended.xml
new file mode 100644
index 000000000..c3e4f2230
--- /dev/null
+++ b/app_pojavlauncher/src/main/res/layout/view_mod_extended.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app_pojavlauncher/src/main/res/values/colors.xml b/app_pojavlauncher/src/main/res/values/colors.xml
index 847f9ec78..ed07e6cbd 100644
--- a/app_pojavlauncher/src/main/res/values/colors.xml
+++ b/app_pojavlauncher/src/main/res/values/colors.xml
@@ -5,6 +5,7 @@
#57CC33
#181818
+ #464646
#242424
#232323
diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml
index b71880696..936c8fd17 100644
--- a/app_pojavlauncher/src/main/res/values/strings.xml
+++ b/app_pojavlauncher/src/main/res/values/strings.xml
@@ -384,6 +384,7 @@
Forces the Minecraft render thread to run on the core with the highest max frequency
Select a version
Downloading installer for %s
+ Searching for Forge version number
Failed to load the version list!
Sorry, but this version of Forge does not have an installer, which is not yet supported.
Select Forge version
@@ -406,7 +407,29 @@
Smoothing time
Reduce jitter in exchange of latency. 0 to disable it
-
+ Search for modpacks
+
+ Install
+ Apply
+
+ No modpacks found
+ Failed to find modpacks
+ Failed to download modpack metadata
+
+ Downloading modpack metadata (%.2f MB / %.2f MB)
+ Downloading mods (%.2f MB / %.2f MB)
+ Downloading mods (File %d out of %d)
+ Applying overrides (%d/%d)
+ Pojav Modpack Installer
+ Click here to finish modpack installation
+ Failed to download mod loader information
+ Failed to download the mod loader files
+ Failed to download modpack files
+ Create modpack profile
+
+ An error has occurred
+ Click to see more details
+
Gestures
Buttons
diff --git a/forge_installer/src/main/java/git/artdeell/installer_agent/Agent.java b/forge_installer/src/main/java/git/artdeell/installer_agent/Agent.java
index dc2cf22c2..53a7361ae 100644
--- a/forge_installer/src/main/java/git/artdeell/installer_agent/Agent.java
+++ b/forge_installer/src/main/java/git/artdeell/installer_agent/Agent.java
@@ -21,11 +21,13 @@ public class Agent implements AWTEventListener {
private boolean forgeWindowHandled = false;
private final boolean suppressProfileCreation;
private final boolean optiFineInstallation;
+ private final String modpackFixupId;
private final Timer componentTimer = new Timer();
- public Agent(boolean nps, boolean of) {
+ public Agent(boolean nps, boolean of, String mf) {
this.suppressProfileCreation = !nps;
this.optiFineInstallation = of;
+ this.modpackFixupId = mf;
}
@Override
@@ -104,7 +106,7 @@ public void handleDialog(Window window) {
JOptionPane optionPane = (JOptionPane) components.get(0);
if(optionPane.getMessageType() == JOptionPane.INFORMATION_MESSAGE) { // forge doesn't emit information messages for other reasons yet
System.out.println("The install was successful!");
- ProfileFixer.reinsertProfile(optiFineInstallation ? "OptiFine" : "forge", suppressProfileCreation);
+ ProfileFixer.reinsertProfile(optiFineInstallation ? "OptiFine" : "forge", modpackFixupId, suppressProfileCreation);
System.exit(0); // again, forge doesn't call exit for some reason, so we do that ourselves here
}
}
@@ -124,13 +126,30 @@ public void insertAllComponents(List components, Container parent, Co
public static void premain(String args, Instrumentation inst) {
boolean noProfileSuppression = false;
boolean optifine = false;
+ String modpackFixupId = null;
if(args != null ) {
- noProfileSuppression = args.contains("NPS"); // No Profile Suppression
- optifine = args.contains("OF"); // OptiFine
+ modpackFixupId = findQuotedString(args);
+ if(modpackFixupId != null) {
+ noProfileSuppression = args.contains("NPS") && !modpackFixupId.contains("NPS");
+ // No Profile Suppression
+ optifine = args.contains("OF") && !modpackFixupId.contains("OF");
+ // OptiFine
+ }else {
+ noProfileSuppression = args.contains("NPS"); // No Profile Suppression
+ optifine = args.contains("OF"); // OptiFine
+ }
}
- Agent agent = new Agent(noProfileSuppression, optifine);
+ Agent agent = new Agent(noProfileSuppression, optifine, modpackFixupId);
Toolkit.getDefaultToolkit()
.addAWTEventListener(agent,
AWTEvent.WINDOW_EVENT_MASK);
}
+
+ private static String findQuotedString(String args) {
+ int quoteIndex = args.indexOf('"');
+ if(quoteIndex == -1) return null;
+ int nextQuoteIndex = args.indexOf('"', quoteIndex+1);
+ if(nextQuoteIndex == -1) return null;
+ return args.substring(quoteIndex+1, nextQuoteIndex);
+ }
}
diff --git a/forge_installer/src/main/java/git/artdeell/installer_agent/ProfileFixer.java b/forge_installer/src/main/java/git/artdeell/installer_agent/ProfileFixer.java
index da6e72967..5dadb711d 100644
--- a/forge_installer/src/main/java/git/artdeell/installer_agent/ProfileFixer.java
+++ b/forge_installer/src/main/java/git/artdeell/installer_agent/ProfileFixer.java
@@ -10,6 +10,7 @@
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Random;
+import java.util.Set;
public class ProfileFixer {
private static final Random random = new Random();
@@ -22,7 +23,8 @@ public static void storeProfile(String profileName) {
StandardCharsets.UTF_8)
);
JSONObject profilesArray = minecraftProfiles.getJSONObject("profiles");
- oldProfile = profilesArray.optJSONObject(profileName, null);
+ profileName = findProfileName(profileName, profilesArray);
+ oldProfile = profileName != null ? minecraftProfiles.getJSONObject(profileName) : null;
}catch (IOException | JSONException e) {
System.out.println("Failed to store Forge profile: "+e);
}
@@ -31,22 +33,24 @@ public static void storeProfile(String profileName) {
private static String pickProfileName(String profileName) {
return profileName+random.nextInt();
}
- public static void reinsertProfile(String profileName, boolean suppressProfileCreation) {
+ public static void reinsertProfile(String profileName, String modpackFixupId, boolean suppressProfileCreation) {
try {
JSONObject minecraftProfiles = new JSONObject(
new String(Files.readAllBytes(profilesPath),
StandardCharsets.UTF_8)
);
JSONObject profilesArray = minecraftProfiles.getJSONObject("profiles");
+ profileName = findProfileName(profileName, profilesArray);
+ if(modpackFixupId != null) fixupModpackProfile(profileName, modpackFixupId, profilesArray);
if(oldProfile != null) {
- if(suppressProfileCreation) profilesArray.put("forge", oldProfile); // restore the old profile
+ if(suppressProfileCreation) profilesArray.put(profileName, oldProfile); // restore the old profile
else {
String name = pickProfileName(profileName);
while(profilesArray.has(name)) name = pickProfileName(profileName);
profilesArray.put(name, oldProfile); // restore the old profile under a new name
}
}else{
- if(suppressProfileCreation) profilesArray.remove("forge"); // remove the new profile
+ if(suppressProfileCreation) profilesArray.remove(profileName); // remove the new profile
// otherwise it wont be removed
}
minecraftProfiles.put("profiles", profilesArray);
@@ -56,4 +60,35 @@ public static void reinsertProfile(String profileName, boolean suppressProfileCr
System.out.println("Failed to restore old Forge profile: "+e);
}
}
+
+ private static void fixupModpackProfile(String profileId, String expectedVersionId, JSONObject profilesArray) {
+ System.out.println("Fixing up modpack profile version ID...");
+ JSONObject modloaderProfile = profilesArray.optJSONObject(profileId);
+ if(modloaderProfile == null) {
+ System.out.println("Failed to find the modloader profile, keys:" + profilesArray.keySet().toString());
+ return;
+ }
+ String modloaderVersionId = modloaderProfile.optString("lastVersionId");
+ if(modloaderVersionId == null) {
+ System.out.println("Failed to find the modloader profile version, keys:" + modloaderProfile.keySet().toString());
+ return;
+ }
+ System.out.println("Expected version ID: "+expectedVersionId+" Modloader version ID: "+modloaderVersionId);
+ if(expectedVersionId.equals(modloaderVersionId)) return;
+ for(String profileKey : profilesArray.keySet()) {
+ if(profileKey.equals(profileId)) continue;
+ JSONObject profile = profilesArray.getJSONObject(profileKey);
+ if(!expectedVersionId.equals(profile.optString("lastVersionId"))) continue;
+ profile.put("lastVersionId", modloaderVersionId);
+ System.out.println("Replacing version ID in profile "+profileKey);
+ }
+ }
+
+ private static String findProfileName(String profileId, JSONObject profilesArray) {
+ Set profiles = profilesArray.keySet();
+ for(String profile : profiles) {
+ if(profile.equalsIgnoreCase(profileId)) return profile;
+ }
+ return null;
+ }
}