Update zink with latest v3_openjdk commits

This commit is contained in:
artdeell
2023-09-13 19:39:53 +03:00
332 changed files with 16185 additions and 53576 deletions

View File

@@ -15,6 +15,7 @@ jobs:
runs-on: ubuntu-22.04
env:
GPLAY_KEYSTORE_PASSWORD: ${{ secrets.GPLAY_KEYSTORE_PASSWORD }}
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v3

3
.gitignore vendored
View File

@@ -5,4 +5,5 @@ app_pojavlauncher/src/main/assets/components/jre
local.properties
.idea/
app_pojavlauncher/.cxx/
.vs/
.vs/
/curseforge_key.txt

View File

@@ -69,6 +69,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
@@ -107,6 +118,7 @@ android {
versionCode getDateSeconds()
versionName getVersionName()
multiDexEnabled true //important
resValue 'string', 'curseforge_api_key', getCFApiKey()
}
buildTypes {
@@ -187,6 +199,7 @@ dependencies {
implementation 'com.github.PojavLauncherTeam:portrait-ssp:6c02fd739b'
implementation 'com.github.Mathias-Boulay:ExtendedView:1.0.0'
implementation 'com.github.Mathias-Boulay:android_gamepad_remapper:eb92e3a5bb'
implementation 'com.github.Mathias-Boulay:virtual-joystick-android:cb7bf45ba5'
// implementation 'com.intuit.sdp:sdp-android:1.0.5'

View File

@@ -7,6 +7,7 @@
android:name="android.hardware.type.pc"
android:required="false" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
@@ -77,11 +78,15 @@
android:name=".FatalErrorActivity"
android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation"
android:theme="@style/Theme.AppCompat.DayNight.Dialog" />
<activity android:name=".ShowErrorActivity"
android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation"
android:theme="@style/Theme.AppCompat.DayNight.Dialog" />
<activity
android:name=".ExitActivity"
android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation"
android:theme="@style/Theme.AppCompat.DayNight.Dialog" />
<activity
android:process=":gui_installer"
android:name=".JavaGUILauncherActivity"
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|keyboard|navigation|uiMode"
android:screenOrientation="sensorLandscape" />

View File

@@ -1 +1 @@
1688133008591
1692525087345

View File

@@ -1 +1 @@
1689180036097
1687078018167

View File

@@ -2,7 +2,7 @@
"profiles": {
"(Default)": {
"name": "(Default)",
"lastVersionId": "Unknown"
"lastVersionId": "1.7.10"
}
},
"selectedProfile": "(Default)"

View File

@@ -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 <T>
*/
public class SimpleArrayAdapter<T> extends BaseAdapter {
private List<T> mObjects;
public SimpleArrayAdapter(List<T> objects) {
setObjects(objects);
}
public void setObjects(@Nullable List<T> 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;
}
}

View File

@@ -46,7 +46,7 @@ public class TextProgressBar extends ProgressBar {
protected synchronized void onDraw(Canvas canvas) {
super.onDraw(canvas);
mTextPaint.setTextSize((float) ((getHeight()- getPaddingBottom() - getPaddingTop()) * 0.55));
int xPos = (int) Math.max(Math.min(getProgress() * getWidth() / getMax(), getWidth() - mTextPaint.measureText(mText)) - mTextPadding, mTextPadding);
int xPos = (int) Math.max(Math.min((getProgress() * getWidth() / getMax()) + mTextPadding, getWidth() - mTextPaint.measureText(mText) - mTextPadding) , mTextPadding);
int yPos = (int) ((getHeight() / 2) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)) ;
canvas.drawText(mText, xPos, yPos, mTextPaint);

View File

@@ -56,7 +56,7 @@ public class mcVersionSpinner extends ExtendedTextView {
private ListView mListView = null;
private PopupWindow mPopupWindow = null;
private Object mPopupAnimation;
private final ProfileAdapter mProfileAdapter = new ProfileAdapter(getContext(), new ProfileAdapterExtra[]{
private final ProfileAdapter mProfileAdapter = new ProfileAdapter(new ProfileAdapterExtra[]{
new ProfileAdapterExtra(VERSION_SPINNER_PROFILE_CREATE,
R.string.create_profile,
ResourcesCompat.getDrawable(getResources(), R.drawable.ic_add, null)),
@@ -77,6 +77,11 @@ public class mcVersionSpinner extends ExtendedTextView {
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

View File

@@ -12,6 +12,7 @@ import androidx.drawerlayout.widget.DrawerLayout;
import net.kdt.pojavlaunch.customcontrols.ControlData;
import net.kdt.pojavlaunch.customcontrols.ControlDrawerData;
import net.kdt.pojavlaunch.customcontrols.ControlJoystickData;
import net.kdt.pojavlaunch.customcontrols.ControlLayout;
import net.kdt.pojavlaunch.customcontrols.EditorExitable;
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
@@ -43,11 +44,11 @@ public class CustomControlsActivity extends BaseActivity implements EditorExitab
switch(position) {
case 0: mControlLayout.addControlButton(new ControlData("New")); break;
case 1: mControlLayout.addDrawer(new ControlDrawerData()); break;
//case 2: mControlLayout.addJoystickButton(new ControlData()); break;
case 2: mControlLayout.openLoadDialog(); break;
case 3: mControlLayout.openSaveDialog(this); break;
case 4: mControlLayout.openSetDefaultDialog(); break;
case 5: // Saving the currently shown control
case 2: mControlLayout.addJoystickButton(new ControlJoystickData()); break;
case 3: mControlLayout.openLoadDialog(); break;
case 4: mControlLayout.openSaveDialog(this); break;
case 5: mControlLayout.openSetDefaultDialog(); break;
case 6: // Saving the currently shown control
try {
Uri contentUri = DocumentsContract.buildDocumentUri(getString(R.string.storageProviderAuthorities), mControlLayout.saveToDirectory(mControlLayout.mLayoutFileName));

View File

@@ -39,6 +39,7 @@ public class JMinecraftVersionList {
public static class JavaVersionInfo {
public String component;
public int majorVersion;
public int version; // parameter used by LabyMod 4
}
@Keep
public static class LoggingConfig {

View File

@@ -55,7 +55,7 @@ public class JRE17Util {
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 class JRE17Util {
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 class JRE17Util {
return false;
} else {
minecraftProfile.javaDir = Tools.LAUNCHERPROFILES_RTPREFIX + JRE17Util.NEW_JRE_NAME;
LauncherProfiles.update();
LauncherProfiles.load();
}
} else {
showRuntimeFail(activity, versionInfo);

View File

@@ -24,6 +24,7 @@ import androidx.fragment.app.FragmentManager;
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.ExtraCore;
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 class LauncherActivity extends BaseActivity {
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 @@ public class LauncherActivity extends BaseActivity {
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 @@ public class LauncherActivity extends BaseActivity {
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;

View File

@@ -322,24 +322,24 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe
}
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 class MainActivity extends BaseActivity implements ControlButtonMenuListe
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) {}

View File

@@ -446,9 +446,11 @@ public class MinecraftGLSurface extends View implements GrabListener {
CallbackBridge.mouseX += (e.getX()* mScaleFactor);
CallbackBridge.mouseY += (e.getY()* mScaleFactor);
// Position is updated by many events, hence it is send regardless of the event value
CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY);
switch (e.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY);
return true;
case MotionEvent.ACTION_BUTTON_PRESS:
return sendMouseButtonUnconverted(e.getActionButton(), true);

View File

@@ -18,6 +18,7 @@ import java.util.concurrent.LinkedBlockingQueue;
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 class PojavApplication extends Application {
startActivity(ferrorIntent);
}
}
@Override
@Override
public void onTerminate() {
super.onTerminate();
ContextExecutor.clearApplication();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(LocaleUtils.setLocale(base));
}

View File

@@ -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());
}
}

View File

@@ -33,6 +33,7 @@ import android.view.View;
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 androidx.fragment.app.FragmentTransaction;
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.JSONUtils;
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 @@ import java.util.Map;
@SuppressWarnings("IOStreamConstructor")
public final class Tools {
public static final float BYTE_TO_MB = 1024 * 1024;
public static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
public static String APP_NAME = "null";
@@ -173,7 +177,7 @@ public final class Tools {
}
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 @@ public final class Tools {
}
}
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));
}
@@ -618,6 +639,53 @@ public final class Tools {
}
return true; // allow if none match
}
private static void preProcessLibraries(DependentLibrary[] libraries) {
for (int i = 0; i < libraries.length; i++) {
DependentLibrary libItem = libraries[i];
String[] version = libItem.name.split(":")[2].split("\\.");
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) 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";
libItem.downloads.artifact.url = "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar";
} else if (libItem.name.startsWith("com.github.oshi:oshi-core:")) {
//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) 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<String> libDir = new ArrayList<>();
for (DependentLibrary libItem: info.libraries) {
@@ -636,7 +704,7 @@ public final class Tools {
try {
JMinecraftVersionList.Version customVer = Tools.GLOBAL_GSON.fromJson(read(DIR_HOME_VERSION + "/" + versionName + "/" + versionName + ".json"), JMinecraftVersionList.Version.class);
if (skipInheriting || customVer.inheritsFrom == null || customVer.inheritsFrom.equals(customVer.id)) {
return customVer;
preProcessLibraries(customVer.libraries);
} else {
JMinecraftVersionList.Version inheritsVer;
//If it won't download, just search for it
@@ -674,6 +742,7 @@ public final class Tools {
}
} finally {
inheritsVer.libraries = libList.toArray(new DependentLibrary[0]);
preProcessLibraries(inheritsVer.libraries);
}
// Inheriting Minecraft 1.13+ with append custom args
@@ -711,8 +780,14 @@ public final class Tools {
inheritsVer.arguments.game = totalArgList.toArray(new Object[0]);
}
return inheritsVer;
customVer = inheritsVer;
}
// LabyMod 4 sets version instead of majorVersion
if (customVer.javaVersion != null && customVer.javaVersion.majorVersion == 0) {
customVer.javaVersion.majorVersion = customVer.javaVersion.version;
}
return customVer;
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -810,7 +885,7 @@ public final class Tools {
public static int getDisplayFriendlyRes(int displaySideRes, float scaling){
displaySideRes *= scaling;
if(displaySideRes % 2 != 0) displaySideRes ++;
if(displaySideRes % 2 != 0) displaySideRes --;
return displaySideRes;
}
@@ -993,4 +1068,12 @@ public final class Tools {
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();
}
}

View File

@@ -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<Application> sApplication;
private static WeakReference<Activity> 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> T getWeakReference(WeakReference<T> weakReference) {
if(weakReference == null) return null;
return weakReference.get();
}
}

View File

@@ -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);
}

View File

@@ -32,21 +32,19 @@ public class ControlData {
public static final int SPECIALBTN_SCROLLUP = -7;
public static final int SPECIALBTN_SCROLLDOWN = -8;
public static final int SPECIALBTN_MENU = -9;
private static ControlData[] SPECIAL_BUTTONS;
private static List<String> SPECIAL_BUTTON_NAME_ARRAY;
// Internal usage only
public boolean isHideable;
private static WeakReference<ExpressionBuilder> builder = new WeakReference<>(null);
private static WeakReference<ArrayMap<String , String>> conversionMap = new WeakReference<>(null);
private static WeakReference<ArrayMap<String, String>> conversionMap = new WeakReference<>(null);
static {
buildExpressionBuilder();
buildConversionMap();
}
// Internal usage only
public boolean isHideable;
/**
* Both fields below are dynamic position data, auto updates
* X and Y position, unlike the original one which uses fixed
@@ -56,60 +54,29 @@ public class ControlData {
*/
public String dynamicX, dynamicY;
public boolean isDynamicBtn, isToggle, passThruEnabled;
public static ControlData[] getSpecialButtons(){
if (SPECIAL_BUTTONS == null) {
SPECIAL_BUTTONS = new ControlData[]{
new ControlData("Keyboard", new int[]{SPECIALBTN_KEYBOARD}, "${margin} * 3 + ${width} * 2", "${margin}", false),
new ControlData("GUI", new int[]{SPECIALBTN_TOGGLECTRL}, "${margin}", "${bottom} - ${margin}"),
new ControlData("PRI", new int[]{SPECIALBTN_MOUSEPRI}, "${margin}", "${screen_height} - ${margin} * 3 - ${height} * 3"),
new ControlData("SEC", new int[]{SPECIALBTN_MOUSESEC}, "${margin} * 3 + ${width} * 2", "${screen_height} - ${margin} * 3 - ${height} * 3"),
new ControlData("Mouse", new int[]{SPECIALBTN_VIRTUALMOUSE}, "${right}", "${margin}", false),
new ControlData("MID", new int[]{SPECIALBTN_MOUSEMID}, "${margin}", "${margin}"),
new ControlData("SCROLLUP", new int[]{SPECIALBTN_SCROLLUP}, "${margin}", "${margin}"),
new ControlData("SCROLLDOWN", new int[]{SPECIALBTN_SCROLLDOWN}, "${margin}", "${margin}"),
new ControlData("MENU", new int[]{SPECIALBTN_MENU}, "${margin}", "${margin}")
};
}
return SPECIAL_BUTTONS;
}
public static List<String> buildSpecialButtonArray() {
if (SPECIAL_BUTTON_NAME_ARRAY == null) {
List<String> nameList = new ArrayList<>();
for (ControlData btn : getSpecialButtons()) {
nameList.add("SPECIAL_" + btn.name);
}
SPECIAL_BUTTON_NAME_ARRAY = nameList;
Collections.reverse(SPECIAL_BUTTON_NAME_ARRAY);
}
return SPECIAL_BUTTON_NAME_ARRAY;
}
public String name;
private float width; //Dp instead of Px now
private float height; //Dp instead of Px now
public int[] keycodes; //Should store up to 4 keys
public float opacity; //Alpha value from 0 to 1;
public int bgColor;
public int strokeColor;
public int strokeWidth; //0-100%
public float strokeWidth; // Dp instead of % now
public float cornerRadius; //0-100%
public boolean isSwipeable;
public boolean displayInGame;
public boolean displayInMenu;
private float width; //Dp instead of Px now
private float height; //Dp instead of Px now
public ControlData() {
this("button");
}
public ControlData(String name){
this(name, new int[] {});
public ControlData(String name) {
this(name, new int[]{});
}
public ControlData(String name, int[] keycodes) {
this(name, keycodes, Tools.currentDisplayMetrics.widthPixels/2f, Tools.currentDisplayMetrics.heightPixels/2f);
this(name, keycodes, Tools.currentDisplayMetrics.widthPixels / 2f, Tools.currentDisplayMetrics.heightPixels / 2f);
}
public ControlData(String name, int[] keycodes, float x, float y) {
@@ -141,11 +108,11 @@ public class ControlData {
this(name, keycodes, dynamicX, dynamicY, isSquare ? 50 : 80, isSquare ? 50 : 30, false);
}
public ControlData(String name, int[] keycodes, String dynamicX, String dynamicY, float width, float height, boolean isToggle){
this(name, keycodes, dynamicX, dynamicY, width, height, isToggle, 1,0x4D000000, 0xFFFFFFFF,0,0);
public ControlData(String name, int[] keycodes, String dynamicX, String dynamicY, float width, float height, boolean isToggle) {
this(name, keycodes, dynamicX, dynamicY, width, height, isToggle, 1, 0x4D000000, 0xFFFFFFFF, 0, 0, true, true);
}
public ControlData(String name, int[] keycodes, String dynamicX, String dynamicY, float width, float height, boolean isToggle, float opacity, int bgColor, int strokeColor, int strokeWidth, float cornerRadius) {
public ControlData(String name, int[] keycodes, String dynamicX, String dynamicY, float width, float height, boolean isToggle, float opacity, int bgColor, int strokeColor, float strokeWidth, float cornerRadius, boolean displayInGame, boolean displayInMenu) {
this.name = name;
this.keycodes = inflateKeycodeArray(keycodes);
this.dynamicX = dynamicX;
@@ -159,10 +126,12 @@ public class ControlData {
this.strokeColor = strokeColor;
this.strokeWidth = strokeWidth;
this.cornerRadius = cornerRadius;
this.displayInGame = displayInGame;
this.displayInMenu = displayInMenu;
}
//Deep copy constructor
public ControlData(ControlData controlData){
public ControlData(ControlData controlData) {
this(
controlData.name,
controlData.keycodes,
@@ -175,17 +144,42 @@ public class ControlData {
controlData.bgColor,
controlData.strokeColor,
controlData.strokeWidth,
controlData.cornerRadius
controlData.cornerRadius,
controlData.displayInGame,
controlData.displayInMenu
);
}
public float insertDynamicPos(String dynamicPos) {
// Insert value to ${variable}
String insertedPos = JSONUtils.insertSingleJSONValue(dynamicPos, fillConversionMap());
// Calculate, because the dynamic position contains some math equations
return calculate(insertedPos);
public static ControlData[] getSpecialButtons() {
if (SPECIAL_BUTTONS == null) {
SPECIAL_BUTTONS = new ControlData[]{
new ControlData("Keyboard", new int[]{SPECIALBTN_KEYBOARD}, "${margin} * 3 + ${width} * 2", "${margin}", false),
new ControlData("GUI", new int[]{SPECIALBTN_TOGGLECTRL}, "${margin}", "${bottom} - ${margin}"),
new ControlData("PRI", new int[]{SPECIALBTN_MOUSEPRI}, "${margin}", "${screen_height} - ${margin} * 3 - ${height} * 3"),
new ControlData("SEC", new int[]{SPECIALBTN_MOUSESEC}, "${margin} * 3 + ${width} * 2", "${screen_height} - ${margin} * 3 - ${height} * 3"),
new ControlData("Mouse", new int[]{SPECIALBTN_VIRTUALMOUSE}, "${right}", "${margin}", false),
new ControlData("MID", new int[]{SPECIALBTN_MOUSEMID}, "${margin}", "${margin}"),
new ControlData("SCROLLUP", new int[]{SPECIALBTN_SCROLLUP}, "${margin}", "${margin}"),
new ControlData("SCROLLDOWN", new int[]{SPECIALBTN_SCROLLDOWN}, "${margin}", "${margin}"),
new ControlData("MENU", new int[]{SPECIALBTN_MENU}, "${margin}", "${margin}")
};
}
return SPECIAL_BUTTONS;
}
public static List<String> buildSpecialButtonArray() {
if (SPECIAL_BUTTON_NAME_ARRAY == null) {
List<String> nameList = new ArrayList<>();
for (ControlData btn : getSpecialButtons()) {
nameList.add("SPECIAL_" + btn.name);
}
SPECIAL_BUTTON_NAME_ARRAY = nameList;
Collections.reverse(SPECIAL_BUTTON_NAME_ARRAY);
}
return SPECIAL_BUTTON_NAME_ARRAY;
}
private static float calculate(String math) {
@@ -193,44 +187,16 @@ public class ControlData {
return (float) builder.get().build().evaluate();
}
private static int[] inflateKeycodeArray(int[] keycodes){
private static int[] inflateKeycodeArray(int[] keycodes) {
int[] inflatedArray = new int[]{GLFW_KEY_UNKNOWN, GLFW_KEY_UNKNOWN, GLFW_KEY_UNKNOWN, GLFW_KEY_UNKNOWN};
System.arraycopy(keycodes, 0, inflatedArray, 0, keycodes.length);
return inflatedArray;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean containsKeycode(int keycodeToCheck){
for(int keycode : keycodes)
if(keycodeToCheck == keycode)
return true;
return false;
}
//Getters || setters (with conversion for ease of use)
public float getWidth() {
return Tools.dpToPx(width);
}
public float getHeight(){
return Tools.dpToPx(height);
}
public void setWidth(float widthInPx){
width = Tools.pxToDp(widthInPx);
}
public void setHeight(float heightInPx){
height = Tools.pxToDp(heightInPx);
}
/**
* Create a builder, keep a weak reference to it to use it with all views on first inflation
*/
private static void buildExpressionBuilder(){
private static void buildExpressionBuilder() {
ExpressionBuilder expressionBuilder = new ExpressionBuilder("1 + 1")
.function(new Function("dp", 1) {
@Override
@@ -249,10 +215,11 @@ public class ControlData {
/**
* wrapper for the WeakReference to the expressionField.
*
* @param stringExpression the expression to set.
*/
private static void setExpression(String stringExpression){
if(builder.get() == null) buildExpressionBuilder();
private static void setExpression(String stringExpression) {
if (builder.get() == null) buildExpressionBuilder();
builder.get().expression(stringExpression);
}
@@ -269,7 +236,7 @@ public class ControlData {
keyValueMap.put("bottom", "DUMMY_BOTTOM");
keyValueMap.put("width", "DUMMY_WIDTH");
keyValueMap.put("height", "DUMMY_HEIGHT");
keyValueMap.put("screen_width", "DUMMY_DATA" );
keyValueMap.put("screen_width", "DUMMY_DATA");
keyValueMap.put("screen_height", "DUMMY_DATA");
keyValueMap.put("margin", Integer.toString((int) Tools.dpToPx(2)));
keyValueMap.put("preferred_scale", "DUMMY_DATA");
@@ -277,14 +244,49 @@ public class ControlData {
conversionMap = new WeakReference<>(keyValueMap);
}
public float insertDynamicPos(String dynamicPos) {
// Insert value to ${variable}
String insertedPos = JSONUtils.insertSingleJSONValue(dynamicPos, fillConversionMap());
// Calculate, because the dynamic position contains some math equations
return calculate(insertedPos);
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean containsKeycode(int keycodeToCheck) {
for (int keycode : keycodes)
if (keycodeToCheck == keycode)
return true;
return false;
}
//Getters || setters (with conversion for ease of use)
public float getWidth() {
return Tools.dpToPx(width);
}
public void setWidth(float widthInPx) {
width = Tools.pxToDp(widthInPx);
}
public float getHeight() {
return Tools.dpToPx(height);
}
public void setHeight(float heightInPx) {
height = Tools.pxToDp(heightInPx);
}
/**
* Fill the conversionMap with controlData dependent values.
* The returned valueMap should NOT be kept in memory.
*
* @return the valueMap to use.
*/
private Map<String, String> fillConversionMap(){
private Map<String, String> fillConversionMap() {
ArrayMap<String, String> valueMap = conversionMap.get();
if (valueMap == null){
if (valueMap == null) {
buildConversionMap();
valueMap = conversionMap.get();
}
@@ -293,8 +295,8 @@ public class ControlData {
valueMap.put("bottom", Float.toString(CallbackBridge.physicalHeight - getHeight()));
valueMap.put("width", Float.toString(getWidth()));
valueMap.put("height", Float.toString(getHeight()));
valueMap.put("screen_width",Integer.toString(CallbackBridge.physicalWidth));
valueMap.put("screen_height",Integer.toString(CallbackBridge.physicalHeight));
valueMap.put("screen_width", Integer.toString(CallbackBridge.physicalWidth));
valueMap.put("screen_height", Integer.toString(CallbackBridge.physicalHeight));
valueMap.put("preferred_scale", Float.toString(LauncherPreferences.PREF_BUTTONSIZE));
return valueMap;

View File

@@ -0,0 +1,15 @@
package net.kdt.pojavlaunch.customcontrols;
public class ControlJoystickData extends ControlData {
/* Whether the joystick can stay forward */
public boolean forwardLock = false;
public ControlJoystickData(){
super();
}
public ControlJoystickData(ControlData properties) {
super(properties);
}
}

View File

@@ -1,8 +1,11 @@
package net.kdt.pojavlaunch.customcontrols;
import static android.content.Context.INPUT_METHOD_SERVICE;
import static net.kdt.pojavlaunch.MainActivity.mControlLayout;
import static net.kdt.pojavlaunch.Tools.currentDisplayMetrics;
import static org.lwjgl.glfw.CallbackBridge.isGrabbing;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
@@ -27,6 +30,7 @@ import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.customcontrols.buttons.ControlButton;
import net.kdt.pojavlaunch.customcontrols.buttons.ControlDrawer;
import net.kdt.pojavlaunch.customcontrols.buttons.ControlInterface;
import net.kdt.pojavlaunch.customcontrols.buttons.ControlJoystick;
import net.kdt.pojavlaunch.customcontrols.buttons.ControlSubButton;
import net.kdt.pojavlaunch.customcontrols.handleview.ActionRow;
import net.kdt.pojavlaunch.customcontrols.handleview.ControlHandleView;
@@ -107,6 +111,12 @@ public class ControlLayout extends FrameLayout {
if(mModifiable) drawer.areButtonsVisible = true;
}
// Joystick(s)
for(ControlJoystickData joystick : mLayout.mJoystickDataList){
addJoystickView(joystick);
}
mLayout.scaledAt = LauncherPreferences.PREF_BUTTONSIZE;
setModified(false);
@@ -177,15 +187,26 @@ public class ControlLayout extends FrameLayout {
view.setFocusable(false);
view.setFocusableInTouchMode(false);
}else{
view.setVisible(drawer.areButtonsVisible);
view.setVisible(true);
}
drawer.addButton(view);
addView(view);
drawer.addButton(view);
setModified(true);
}
// JOYSTICK BUTTON
public void addJoystickButton(ControlJoystickData data){
mLayout.mJoystickDataList.add(data);
addJoystickView(data);
}
private void addJoystickView(ControlJoystickData data){
addView(new ControlJoystick(this, data));
}
private void removeAllButtons() {
for(ControlInterface button : getButtonChildren()){
@@ -220,7 +241,7 @@ public class ControlLayout extends FrameLayout {
mControlVisible = isVisible;
for(ControlInterface button : getButtonChildren()){
button.setVisible(isVisible);
button.setVisible(((button.getProperties().displayInGame && isGrabbing()) || (button.getProperties().displayInMenu && !isGrabbing())) && isVisible);
}
}
@@ -229,6 +250,12 @@ public class ControlLayout extends FrameLayout {
removeEditWindow();
}
mModifiable = isModifiable;
if(isModifiable){
// In edit mode, all controls have to be shown
for(ControlInterface button : getButtonChildren()){
button.setVisible(true);
}
}
}
public boolean getModifiable(){
@@ -556,4 +583,8 @@ public class ControlLayout extends FrameLayout {
builder.setNegativeButton(R.string.global_no, (d,w)->{});
builder.show();
}
public boolean areControlVisible(){
return mControlVisible;
}
}

View File

@@ -13,16 +13,18 @@ public class CustomControls {
public float scaledAt;
public List<ControlData> mControlDataList;
public List<ControlDrawerData> mDrawerDataList;
public List<ControlJoystickData> mJoystickDataList;
public CustomControls() {
this(new ArrayList<>(), new ArrayList<>());
this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
}
public CustomControls(List<ControlData> mControlDataList, List<ControlDrawerData> mDrawerDataList) {
public CustomControls(List<ControlData> mControlDataList, List<ControlDrawerData> mDrawerDataList, List<ControlJoystickData> mJoystickDataList) {
this.mControlDataList = mControlDataList;
this.mDrawerDataList = mDrawerDataList;
this.scaledAt = 100f;
this.mJoystickDataList = mJoystickDataList;
this.scaledAt = 100f;
}
// Generate default control
@@ -51,14 +53,14 @@ public class CustomControls {
this.mControlDataList.add(shiftData);
this.mControlDataList.add(new ControlData(ctx, R.string.control_jump, new int[]{LwjglGlfwKeycode.GLFW_KEY_SPACE}, "${right} - ${margin} * 2 - ${width}", "${bottom} - ${margin} * 2 - ${height}", true));
//The default controls are conform to the V2
version = 4;
//The default controls are conform to the V3
version = 6;
}
public void save(String path) throws IOException {
//Current version is the V2.5 so the version as to be marked as 4 !
version = 4;
//Current version is the V3.0 so the version as to be marked as 6 !
version = 6;
Tools.write(path, Tools.GLOBAL_GSON.toJson(this));
}

View File

@@ -20,105 +20,159 @@ public class LayoutConverter {
try {
JSONObject layoutJobj = new JSONObject(jsonLayoutData);
if(!layoutJobj.has("version")) { //v1 layout
if (!layoutJobj.has("version")) { //v1 layout
CustomControls layout = LayoutConverter.convertV1Layout(layoutJobj);
layout.save(jsonPath);
return layout;
}else if (layoutJobj.getInt("version") == 2) {
} else if (layoutJobj.getInt("version") == 2) {
CustomControls layout = LayoutConverter.convertV2Layout(layoutJobj);
layout.save(jsonPath);
return layout;
}else if (layoutJobj.getInt("version") == 3 || layoutJobj.getInt("version") == 4) {
}else if (layoutJobj.getInt("version") >= 3 && layoutJobj.getInt("version") <= 5) {
return LayoutConverter.convertV3_4Layout(layoutJobj);
} else if (layoutJobj.getInt("version") == 6) {
return Tools.GLOBAL_GSON.fromJson(jsonLayoutData, CustomControls.class);
}else{
} else {
return null;
}
}catch (JSONException e) {
throw new JsonSyntaxException("Failed to load",e);
} catch (JSONException e) {
throw new JsonSyntaxException("Failed to load", e);
}
}
/**
* Normalize the layout to v6 from v3/4: The stroke width is no longer dependant on the button size
*/
public static CustomControls convertV3_4Layout(JSONObject oldLayoutJson) {
CustomControls layout = Tools.GLOBAL_GSON.fromJson(oldLayoutJson.toString(), CustomControls.class);
convertStrokeWidth(layout);
layout.version = 6;
return layout;
}
public static CustomControls convertV2Layout(JSONObject oldLayoutJson) throws JSONException {
CustomControls layout = Tools.GLOBAL_GSON.fromJson(oldLayoutJson.toString(), CustomControls.class);
JSONArray layoutMainArray = oldLayoutJson.getJSONArray("mControlDataList");
layout.mControlDataList = new ArrayList<>(layoutMainArray.length());
for(int i = 0; i < layoutMainArray.length(); i++) {
for (int i = 0; i < layoutMainArray.length(); i++) {
JSONObject button = layoutMainArray.getJSONObject(i);
ControlData n_button = Tools.GLOBAL_GSON.fromJson(button.toString(), ControlData.class);
if(!Tools.isValidString(n_button.dynamicX) && button.has("x")) {
if (!Tools.isValidString(n_button.dynamicX) && button.has("x")) {
double buttonC = button.getDouble("x");
double ratio = buttonC/CallbackBridge.physicalWidth;
double ratio = buttonC / CallbackBridge.physicalWidth;
n_button.dynamicX = ratio + " * ${screen_width}";
}
if(!Tools.isValidString(n_button.dynamicY) && button.has("y")) {
if (!Tools.isValidString(n_button.dynamicY) && button.has("y")) {
double buttonC = button.getDouble("y");
double ratio = buttonC/CallbackBridge.physicalHeight;
double ratio = buttonC / CallbackBridge.physicalHeight;
n_button.dynamicY = ratio + " * ${screen_height}";
}
layout.mControlDataList.add(n_button);
}
JSONArray layoutDrawerArray = oldLayoutJson.getJSONArray("mDrawerDataList");
layout.mDrawerDataList = new ArrayList<>();
for(int i = 0; i < layoutDrawerArray.length(); i++) {
for (int i = 0; i < layoutDrawerArray.length(); i++) {
JSONObject button = layoutDrawerArray.getJSONObject(i);
JSONObject buttonProperties = button.getJSONObject("properties");
ControlDrawerData n_button = Tools.GLOBAL_GSON.fromJson(button.toString(), ControlDrawerData.class);
if(!Tools.isValidString(n_button.properties.dynamicX) && buttonProperties.has("x")) {
if (!Tools.isValidString(n_button.properties.dynamicX) && buttonProperties.has("x")) {
double buttonC = buttonProperties.getDouble("x");
double ratio = buttonC/CallbackBridge.physicalWidth;
double ratio = buttonC / CallbackBridge.physicalWidth;
n_button.properties.dynamicX = ratio + " * ${screen_width}";
}
if(!Tools.isValidString(n_button.properties.dynamicY) && buttonProperties.has("y")) {
if (!Tools.isValidString(n_button.properties.dynamicY) && buttonProperties.has("y")) {
double buttonC = buttonProperties.getDouble("y");
double ratio = buttonC/CallbackBridge.physicalHeight;
double ratio = buttonC / CallbackBridge.physicalHeight;
n_button.properties.dynamicY = ratio + " * ${screen_height}";
}
layout.mDrawerDataList.add(n_button);
}
convertStrokeWidth(layout);
layout.version = 3;
return layout;
}
public static CustomControls convertV1Layout(JSONObject oldLayoutJson) throws JSONException {
CustomControls empty = new CustomControls();
JSONArray layoutMainArray = oldLayoutJson.getJSONArray("mControlDataList");
for(int i = 0; i < layoutMainArray.length(); i++) {
for (int i = 0; i < layoutMainArray.length(); i++) {
JSONObject button = layoutMainArray.getJSONObject(i);
ControlData n_button = new ControlData();
int[] keycodes = new int[] {LwjglGlfwKeycode.GLFW_KEY_UNKNOWN,
int[] keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_UNKNOWN,
LwjglGlfwKeycode.GLFW_KEY_UNKNOWN,
LwjglGlfwKeycode.GLFW_KEY_UNKNOWN,
LwjglGlfwKeycode.GLFW_KEY_UNKNOWN};
n_button.isDynamicBtn = button.getBoolean("isDynamicBtn");
n_button.dynamicX = button.getString("dynamicX");
n_button.dynamicY = button.getString("dynamicY");
if(!Tools.isValidString(n_button.dynamicX) && button.has("x")) {
if (!Tools.isValidString(n_button.dynamicX) && button.has("x")) {
double buttonC = button.getDouble("x");
double ratio = buttonC/CallbackBridge.physicalWidth;
double ratio = buttonC / CallbackBridge.physicalWidth;
n_button.dynamicX = ratio + " * ${screen_width}";
}
if(!Tools.isValidString(n_button.dynamicY) && button.has("y")) {
if (!Tools.isValidString(n_button.dynamicY) && button.has("y")) {
double buttonC = button.getDouble("y");
double ratio = buttonC/CallbackBridge.physicalHeight;
double ratio = buttonC / CallbackBridge.physicalHeight;
n_button.dynamicY = ratio + " * ${screen_height}";
}
n_button.name = button.getString("name");
n_button.opacity = ((float)((button.getInt("transparency")-100)*-1))/100f;
n_button.opacity = ((float) ((button.getInt("transparency") - 100) * -1)) / 100f;
n_button.passThruEnabled = button.getBoolean("passThruEnabled");
n_button.isToggle = button.getBoolean("isToggle");
n_button.setHeight(button.getInt("height"));
n_button.setWidth(button.getInt("width"));
n_button.bgColor = 0x4d000000;
n_button.strokeWidth = 0;
if(button.getBoolean("isRound")) { n_button.cornerRadius = 35f; }
if (button.getBoolean("isRound")) {
n_button.cornerRadius = 35f;
}
int next_idx = 0;
if(button.getBoolean("holdShift")) { keycodes[next_idx] = LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT; next_idx++; }
if(button.getBoolean("holdCtrl")) { keycodes[next_idx] = LwjglGlfwKeycode.GLFW_KEY_LEFT_CONTROL; next_idx++; }
if(button.getBoolean("holdAlt")) { keycodes[next_idx] = LwjglGlfwKeycode.GLFW_KEY_LEFT_ALT; next_idx++; }
if (button.getBoolean("holdShift")) {
keycodes[next_idx] = LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT;
next_idx++;
}
if (button.getBoolean("holdCtrl")) {
keycodes[next_idx] = LwjglGlfwKeycode.GLFW_KEY_LEFT_CONTROL;
next_idx++;
}
if (button.getBoolean("holdAlt")) {
keycodes[next_idx] = LwjglGlfwKeycode.GLFW_KEY_LEFT_ALT;
next_idx++;
}
keycodes[next_idx] = button.getInt("keycode");
n_button.keycodes = keycodes;
empty.mControlDataList.add(n_button);
}
empty.scaledAt = (float)oldLayoutJson.getDouble("scaledAt");
empty.scaledAt = (float) oldLayoutJson.getDouble("scaledAt");
empty.version = 3;
return empty;
}
/**
* Convert the layout stroke width to the V5 form
*/
private static void convertStrokeWidth(CustomControls layout) {
for (ControlData data : layout.mControlDataList) {
data.strokeWidth = Tools.pxToDp(computeStrokeWidth(data.strokeWidth, data.getWidth(), data.getHeight()));
}
for (ControlDrawerData data : layout.mDrawerDataList) {
data.properties.strokeWidth = Tools.pxToDp(computeStrokeWidth(data.properties.strokeWidth, data.properties.getWidth(), data.properties.getHeight()));
for (ControlData subButtonData : data.buttonProperties) {
subButtonData.strokeWidth = Tools.pxToDp(computeStrokeWidth(subButtonData.strokeWidth, data.properties.getWidth(), data.properties.getWidth()));
}
}
}
/**
* Convert a size percentage into a px size, used by older layout versions
*/
static int computeStrokeWidth(float widthInPercent, float width, float height) {
float maxSize = Math.max(width, height);
return (int) ((maxSize / 2) * (widthInPercent / 100));
}
}

View File

@@ -31,6 +31,9 @@ public class ControlButton extends TextView implements ControlInterface {
protected ControlData mProperties;
private final ControlLayout mControlLayout;
/* Cache value from the ControlData radius for drawing purposes */
private float mComputedRadius;
protected boolean mIsToggled = false;
protected boolean mIsPointerOutOfBounds = false;
@@ -42,6 +45,7 @@ public class ControlButton extends TextView implements ControlInterface {
setTextColor(Color.WHITE);
setPadding(4, 4, 4, 4);
setTextSize(14); // Nullify the default size setting
setOutlineProvider(null); // Disable shadow casting, removing one drawing pass
//setOnLongClickListener(this);
@@ -61,6 +65,7 @@ public class ControlButton extends TextView implements ControlInterface {
public void setProperties(ControlData properties, boolean changePos) {
mProperties = properties;
ControlInterface.super.setProperties(properties, changePos);
mComputedRadius = ControlInterface.super.computeCornerRadius(mProperties.cornerRadius);
if (mProperties.isToggle) {
//For the toggle layer
@@ -76,16 +81,11 @@ public class ControlButton extends TextView implements ControlInterface {
setText(properties.name);
}
public void setVisible(boolean isVisible){
if(mProperties.isHideable)
setVisibility(isVisible ? VISIBLE : GONE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mIsToggled || (!mProperties.isToggle && isActivated()))
canvas.drawRoundRect(0, 0, getWidth(), getHeight(), mProperties.cornerRadius, mProperties.cornerRadius, mRectPaint);
canvas.drawRoundRect(0, 0, getWidth(), getHeight(), mComputedRadius, mComputedRadius, mRectPaint);
}

View File

@@ -2,7 +2,9 @@ package net.kdt.pojavlaunch.customcontrols.buttons;
import android.annotation.SuppressLint;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.customcontrols.ControlData;
@@ -40,18 +42,19 @@ public class ControlDrawer extends ControlButton {
public void addButton(ControlSubButton button){
buttons.add(button);
setControlButtonVisibility(button, areButtonsVisible);
syncButtons();
setControlButtonVisibility(button, areButtonsVisible);
}
private void setControlButtonVisibility(ControlButton button, boolean isVisible){
button.setVisible(isVisible);
button.getControlView().setVisibility(isVisible ? VISIBLE : GONE);
}
private void switchButtonVisibility(){
areButtonsVisible = !areButtonsVisible;
int visibility = areButtonsVisible ? VISIBLE : GONE;
for(ControlButton button : buttons){
button.setVisible(areButtonsVisible);
button.getControlView().setVisibility(visibility);
}
}
@@ -88,7 +91,7 @@ public class ControlDrawer extends ControlButton {
private void resizeButtons(){
if (buttons == null) return;
if (buttons == null || drawerData.orientation == ControlDrawerData.Orientation.FREE) return;
for(ControlSubButton subButton : buttons){
subButton.mProperties.setWidth(mProperties.getWidth());
subButton.mProperties.setHeight(mProperties.getHeight());
@@ -124,8 +127,13 @@ public class ControlDrawer extends ControlButton {
@Override
public void setVisible(boolean isVisible) {
//TODO replicate changes to his children ?
setVisibility(isVisible ? VISIBLE : GONE);
int visibility = isVisible ? VISIBLE : GONE;
setVisibility(visibility);
if(visibility == GONE || areButtonsVisible) {
for(ControlSubButton button : buttons){
button.getControlView().setVisibility(isVisible ? VISIBLE : (!mProperties.isHideable && getVisibility() == GONE) ? VISIBLE : View.GONE);
}
}
}
@SuppressLint("ClickableViewAccessibility")

View File

@@ -1,17 +1,22 @@
package net.kdt.pojavlaunch.customcontrols.buttons;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_BUTTONSIZE;
import android.annotation.SuppressLint;
import android.graphics.drawable.GradientDrawable;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.core.math.MathUtils;
import net.kdt.pojavlaunch.GrabListener;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.customcontrols.ControlData;
import net.kdt.pojavlaunch.customcontrols.ControlLayout;
@@ -24,34 +29,54 @@ import org.lwjgl.glfw.CallbackBridge;
* Most of the injected behavior is editing behavior,
* sending keys has to be implemented by sub classes.
*/
public interface ControlInterface extends View.OnLongClickListener {
public interface ControlInterface extends View.OnLongClickListener, GrabListener {
View getControlView();
ControlData getProperties();
/** Remove the button presence from the CustomControl object
default void setProperties(ControlData properties) {
setProperties(properties, true);
}
/**
* Remove the button presence from the CustomControl object
* You need to use {getControlParent()} for this.
*/
void removeButton();
/** Duplicate the data of the button and add a view with the duplicated data
/**
* Duplicate the data of the button and add a view with the duplicated data
* Relies on the ControlLayout for the implementation.
*/
void cloneButton();
void setVisible(boolean isVisible);
default void setVisible(boolean isVisible) {
if(getProperties().isHideable)
getControlView().setVisibility(isVisible ? VISIBLE : GONE);
}
void sendKeyPresses(boolean isDown);
/** Load the values and hide non useful forms */
/**
* Load the values and hide non useful forms
*/
void loadEditValues(EditControlPopup editControlPopup);
@Override
default void onGrabState(boolean isGrabbing) {
if (getControlLayoutParent() != null && getControlLayoutParent().getModifiable()) return; // Disable when edited
setVisible(((getProperties().displayInGame && isGrabbing) || (getProperties().displayInMenu && !isGrabbing)) && getControlLayoutParent().areControlVisible());
}
default ControlLayout getControlLayoutParent(){
default ControlLayout getControlLayoutParent() {
return (ControlLayout) getControlView().getParent();
}
/** Apply conversion steps for when the view is created */
default ControlData preProcessProperties(ControlData properties, ControlLayout layout){
/**
* Apply conversion steps for when the view is created
*/
default ControlData preProcessProperties(ControlData properties, ControlLayout layout) {
//Size
properties.setWidth(properties.getWidth() / layout.getLayoutScale() * PREF_BUTTONSIZE);
properties.setHeight(properties.getHeight() / layout.getLayoutScale() * PREF_BUTTONSIZE);
@@ -66,10 +91,6 @@ public interface ControlInterface extends View.OnLongClickListener {
setProperties(getProperties());
}
default void setProperties(ControlData properties) {
setProperties(properties, true);
}
/* This function should be overridden to store the properties */
@CallSuper
default void setProperties(ControlData properties, boolean changePos) {
@@ -80,19 +101,22 @@ public interface ControlInterface extends View.OnLongClickListener {
// Recycle layout params
ViewGroup.LayoutParams params = getControlView().getLayoutParams();
if(params == null) params = new FrameLayout.LayoutParams((int) properties.getWidth(), (int) properties.getHeight());
if (params == null)
params = new FrameLayout.LayoutParams((int) properties.getWidth(), (int) properties.getHeight());
params.width = (int) properties.getWidth();
params.height = (int) properties.getHeight();
getControlView().setLayoutParams(params);
}
/** Apply the background according to properties */
default void setBackground(){
GradientDrawable gd = getControlView().getBackground() instanceof GradientDrawable
/**
* Apply the background according to properties
*/
default void setBackground() {
GradientDrawable gd = getControlView().getBackground() instanceof GradientDrawable
? (GradientDrawable) getControlView().getBackground()
: new GradientDrawable();
gd.setColor(getProperties().bgColor);
gd.setStroke(computeStrokeWidth(getProperties().strokeWidth), getProperties().strokeColor);
gd.setStroke((int) Tools.dpToPx(getProperties().strokeWidth), getProperties().strokeColor);
gd.setCornerRadius(computeCornerRadius(getProperties().cornerRadius));
getControlView().setBackground(gd);
@@ -100,50 +124,56 @@ public interface ControlInterface extends View.OnLongClickListener {
/**
* Apply the dynamic equation on the x axis.
*
* @param dynamicX The equation to compute the position from
*/
default void setDynamicX(String dynamicX){
default void setDynamicX(String dynamicX) {
getProperties().dynamicX = dynamicX;
getControlView().setX(getProperties().insertDynamicPos(dynamicX));
}
/**
* Apply the dynamic equation on the y axis.
*
* @param dynamicY The equation to compute the position from
*/
default void setDynamicY(String dynamicY){
default void setDynamicY(String dynamicY) {
getProperties().dynamicY = dynamicY;
getControlView().setY(getProperties().insertDynamicPos(dynamicY));
}
/**
* Generate a dynamic equation from an absolute position, used to scale properly across devices
*
* @param x The absolute position on the horizontal axis
* @return The equation as a String
*/
default String generateDynamicX(float x){
if(x + (getProperties().getWidth()/2f) > CallbackBridge.physicalWidth/2f){
default String generateDynamicX(float x) {
if (x + (getProperties().getWidth() / 2f) > CallbackBridge.physicalWidth / 2f) {
return (x + getProperties().getWidth()) / CallbackBridge.physicalWidth + " * ${screen_width} - ${width}";
}else{
} else {
return x / CallbackBridge.physicalWidth + " * ${screen_width}";
}
}
/**
* Generate a dynamic equation from an absolute position, used to scale properly across devices
*
* @param y The absolute position on the vertical axis
* @return The equation as a String
*/
default String generateDynamicY(float y){
if(y + (getProperties().getHeight()/2f) > CallbackBridge.physicalHeight/2f){
return (y + getProperties().getHeight()) / CallbackBridge.physicalHeight + " * ${screen_height} - ${height}";
}else{
default String generateDynamicY(float y) {
if (y + (getProperties().getHeight() / 2f) > CallbackBridge.physicalHeight / 2f) {
return (y + getProperties().getHeight()) / CallbackBridge.physicalHeight + " * ${screen_height} - ${height}";
} else {
return y / CallbackBridge.physicalHeight + " * ${screen_height}";
}
}
/** Regenerate and apply coordinates with supposedly modified properties */
default void regenerateDynamicCoordinates(){
/**
* Regenerate and apply coordinates with supposedly modified properties
*/
default void regenerateDynamicCoordinates() {
getProperties().dynamicX = generateDynamicX(getControlView().getX());
getProperties().dynamicY = generateDynamicY(getControlView().getY());
updateProperties();
@@ -152,30 +182,28 @@ public interface ControlInterface extends View.OnLongClickListener {
/**
* Do a pre-conversion of an equation using values from a button,
* so the variables can be used for another button
*
* <p>
* Internal use only.
*
* @param equation The dynamic position as a String
* @param button The button to get the values from.
* @param button The button to get the values from.
* @return The pre-processed equation as a String.
*/
default String applySize(String equation, ControlInterface button){
default String applySize(String equation, ControlInterface button) {
return equation
.replace("${right}", "(${screen_width} - ${width})")
.replace("${bottom}","(${screen_height} - ${height})")
.replace("${bottom}", "(${screen_height} - ${height})")
.replace("${height}", "(px(" + Tools.pxToDp(button.getProperties().getHeight()) + ") /" + PREF_BUTTONSIZE + " * ${preferred_scale})")
.replace("${width}", "(px(" + Tools.pxToDp(button.getProperties().getWidth()) + ") / " + PREF_BUTTONSIZE + " * ${preferred_scale})");
}
/** Convert a size percentage into a px size */
default int computeStrokeWidth(float widthInPercent){
float maxSize = Math.max(getProperties().getWidth(), getProperties().getHeight());
return (int)((maxSize/2) * (widthInPercent/100));
}
/** Convert a corner radius percentage into a px corner radius */
default float computeCornerRadius(float radiusInPercent){
/**
* Convert a corner radius percentage into a px corner radius
*/
default float computeCornerRadius(float radiusInPercent) {
float minSize = Math.min(getProperties().getWidth(), getProperties().getHeight());
return (minSize/2) * (radiusInPercent/100);
return (minSize / 2) * (radiusInPercent / 100);
}
/**
@@ -185,10 +213,10 @@ public interface ControlInterface extends View.OnLongClickListener {
* @return whether or not the button
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
default boolean canSnap(ControlInterface button){
default boolean canSnap(ControlInterface button) {
float MIN_DISTANCE = Tools.dpToPx(8);
if(button == this) return false;
if (button == this) return false;
return !(net.kdt.pojavlaunch.utils.MathUtils.dist(
button.getControlView().getX() + button.getControlView().getWidth() / 2f,
button.getControlView().getY() + button.getControlView().getHeight() / 2f,
@@ -202,13 +230,13 @@ public interface ControlInterface extends View.OnLongClickListener {
* Try to snap, then align to neighboring buttons, given the provided coordinates.
* The new position is automatically applied to the View,
* regardless of if the View snapped or not.
*
* <p>
* The new position is always dynamic, thus replacing previous dynamic positions
*
* @param x Coordinate on the x axis
* @param y Coordinate on the y axis
*/
default void snapAndAlign(float x, float y){
default void snapAndAlign(float x, float y) {
float MIN_DISTANCE = Tools.dpToPx(8);
String dynamicX = generateDynamicX(x);
String dynamicY = generateDynamicY(y);
@@ -216,9 +244,9 @@ public interface ControlInterface extends View.OnLongClickListener {
getControlView().setX(x);
getControlView().setY(y);
for(ControlInterface button : ((ControlLayout) getControlView().getParent()).getButtonChildren()){
for (ControlInterface button : ((ControlLayout) getControlView().getParent()).getButtonChildren()) {
//Step 1: Filter unwanted buttons
if(!canSnap(button)) continue;
if (!canSnap(button)) continue;
//Step 2: Get Coordinates
float button_top = button.getControlView().getY();
@@ -232,28 +260,28 @@ public interface ControlInterface extends View.OnLongClickListener {
float right = getControlView().getX() + getControlView().getWidth();
//Step 3: For each axis, we try to snap to the nearest
if(Math.abs(top - button_bottom) < MIN_DISTANCE){ // Bottom snap
dynamicY = applySize(button.getProperties().dynamicY, button) + applySize(" + ${height}", button) + " + ${margin}" ;
}else if(Math.abs(button_top - bottom) < MIN_DISTANCE){ //Top snap
if (Math.abs(top - button_bottom) < MIN_DISTANCE) { // Bottom snap
dynamicY = applySize(button.getProperties().dynamicY, button) + applySize(" + ${height}", button) + " + ${margin}";
} else if (Math.abs(button_top - bottom) < MIN_DISTANCE) { //Top snap
dynamicY = applySize(button.getProperties().dynamicY, button) + " - ${height} - ${margin}";
}
if(!dynamicY.equals(generateDynamicY(getControlView().getY()))){ //If we snapped
if(Math.abs(button_left - left) < MIN_DISTANCE){ //Left align snap
if (!dynamicY.equals(generateDynamicY(getControlView().getY()))) { //If we snapped
if (Math.abs(button_left - left) < MIN_DISTANCE) { //Left align snap
dynamicX = applySize(button.getProperties().dynamicX, button);
}else if(Math.abs(button_right - right) < MIN_DISTANCE){ //Right align snap
} else if (Math.abs(button_right - right) < MIN_DISTANCE) { //Right align snap
dynamicX = applySize(button.getProperties().dynamicX, button) + applySize(" + ${width}", button) + " - ${width}";
}
}
if(Math.abs(button_left - right) < MIN_DISTANCE){ //Left snap
if (Math.abs(button_left - right) < MIN_DISTANCE) { //Left snap
dynamicX = applySize(button.getProperties().dynamicX, button) + " - ${width} - ${margin}";
}else if(Math.abs(left - button_right) < MIN_DISTANCE){ //Right snap
} else if (Math.abs(left - button_right) < MIN_DISTANCE) { //Right snap
dynamicX = applySize(button.getProperties().dynamicX, button) + applySize(" + ${width}", button) + " + ${margin}";
}
if(!dynamicX.equals(generateDynamicX(getControlView().getX()))){ //If we snapped
if(Math.abs(button_top - top) < MIN_DISTANCE){ //Top align snap
if (!dynamicX.equals(generateDynamicX(getControlView().getX()))) { //If we snapped
if (Math.abs(button_top - top) < MIN_DISTANCE) { //Top align snap
dynamicY = applySize(button.getProperties().dynamicY, button);
}else if(Math.abs(button_bottom - bottom) < MIN_DISTANCE){ //Bottom align snap
} else if (Math.abs(button_bottom - bottom) < MIN_DISTANCE) { //Bottom align snap
dynamicY = applySize(button.getProperties().dynamicY, button) + applySize(" + ${height}", button) + " - ${height}";
}
}
@@ -264,19 +292,50 @@ public interface ControlInterface extends View.OnLongClickListener {
setDynamicY(dynamicY);
}
/** Wrapper for multiple injections at once */
default void injectBehaviors(){
/**
* Wrapper for multiple injections at once
*/
default void injectBehaviors() {
injectProperties();
injectTouchEventBehavior();
injectLayoutParamBehavior();
injectGrabListenerBehavior();
}
default void injectProperties(){
/**
* Inject the grab listener, remove it when the view is gone
*/
default void injectGrabListenerBehavior() {
if (getControlView() == null) {
Log.e(ControlInterface.class.toString(), "Failed to inject grab listener behavior !");
return;
}
getControlView().addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(@NonNull View v) {
CallbackBridge.addGrabListener(ControlInterface.this);
}
@Override
public void onViewDetachedFromWindow(@NonNull View v) {
getControlView().removeOnAttachStateChangeListener(this);
CallbackBridge.removeGrabListener(ControlInterface.this);
}
});
}
default void injectProperties() {
getControlView().post(() -> getControlView().setTranslationZ(10));
}
/** Inject a touch listener on the view to make editing controls straight forward */
default void injectTouchEventBehavior(){
/**
* Inject a touch listener on the view to make editing controls straight forward
*/
default void injectTouchEventBehavior() {
getControlView().setOnTouchListener(new View.OnTouchListener() {
private boolean mCanTriggerLongClick = true;
private float downX, downY;
@@ -285,7 +344,7 @@ public interface ControlInterface extends View.OnLongClickListener {
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View view, MotionEvent event) {
if(!getControlLayoutParent().getModifiable()){
if (!getControlLayoutParent().getModifiable()) {
// Basically, editing behavior is forced while in game behavior is specific
view.onTouchEvent(event);
return true;
@@ -309,7 +368,7 @@ public interface ControlInterface extends View.OnLongClickListener {
break;
case MotionEvent.ACTION_MOVE:
if(Math.abs(event.getRawX() - downRawX) > 8 || Math.abs(event.getRawY() - downRawY) > 8)
if (Math.abs(event.getRawX() - downRawX) > 8 || Math.abs(event.getRawY() - downRawY) > 8)
mCanTriggerLongClick = false;
getControlLayoutParent().adaptPanelPosition();
@@ -327,17 +386,17 @@ public interface ControlInterface extends View.OnLongClickListener {
});
}
default void injectLayoutParamBehavior(){
default void injectLayoutParamBehavior() {
getControlView().addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
getProperties().setWidth(right-left);
getProperties().setHeight(bottom-top);
getProperties().setWidth(right - left);
getProperties().setHeight(bottom - top);
setBackground();
// Re-calculate position
if(!getProperties().isDynamicBtn){
if (!getProperties().isDynamicBtn) {
getControlView().setX(getControlView().getX());
getControlView().setY(getControlView().getY());
}else {
} else {
getControlView().setX(getProperties().insertDynamicPos(getProperties().dynamicX));
getControlView().setY(getProperties().insertDynamicPos(getProperties().dynamicY));
}
@@ -345,7 +404,7 @@ public interface ControlInterface extends View.OnLongClickListener {
}
@Override
default boolean onLongClick(View v){
default boolean onLongClick(View v) {
if (getControlLayoutParent().getModifiable()) {
getControlLayoutParent().editControlButton(this);
getControlLayoutParent().mActionRow.setFollowedButton(this);

View File

@@ -0,0 +1,166 @@
package net.kdt.pojavlaunch.customcontrols.buttons;
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_EAST;
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_NONE;
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_NORTH;
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_NORTH_EAST;
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_NORTH_WEST;
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_SOUTH;
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_SOUTH_EAST;
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_SOUTH_WEST;
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_WEST;
import android.annotation.SuppressLint;
import android.view.View;
import net.kdt.pojavlaunch.LwjglGlfwKeycode;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.customcontrols.ControlData;
import net.kdt.pojavlaunch.customcontrols.ControlJoystickData;
import net.kdt.pojavlaunch.customcontrols.ControlLayout;
import net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick;
import net.kdt.pojavlaunch.customcontrols.handleview.EditControlPopup;
import org.lwjgl.glfw.CallbackBridge;
import io.github.controlwear.virtual.joystick.android.JoystickView;
@SuppressLint("ViewConstructor")
public class ControlJoystick extends JoystickView implements ControlInterface {
public final static int DIRECTION_FORWARD_LOCK = 8;
// Directions keycode
private final int[] mDirectionForwardLock = new int[]{LwjglGlfwKeycode.GLFW_KEY_LEFT_CONTROL};
private final int[] mDirectionForward = new int[]{LwjglGlfwKeycode.GLFW_KEY_W};
private final int[] mDirectionRight = new int[]{LwjglGlfwKeycode.GLFW_KEY_D};
private final int[] mDirectionBackward = new int[]{LwjglGlfwKeycode.GLFW_KEY_S};
private final int[] mDirectionLeft = new int[]{LwjglGlfwKeycode.GLFW_KEY_A};
private ControlJoystickData mControlData;
private int mLastDirectionInt = GamepadJoystick.DIRECTION_NONE;
private int mCurrentDirectionInt = GamepadJoystick.DIRECTION_NONE;
public ControlJoystick(ControlLayout parent, ControlJoystickData data) {
super(parent.getContext());
init(data, parent);
}
private static void sendInput(int[] keys, boolean isDown) {
for (int key : keys) {
CallbackBridge.sendKeyPress(key, CallbackBridge.getCurrentMods(), isDown);
}
}
private void init(ControlJoystickData data, ControlLayout layout) {
mControlData = data;
setProperties(preProcessProperties(data, layout));
setDeadzone(35);
setFixedCenter(false);
setAutoReCenterButton(true);
injectBehaviors();
setOnMoveListener(new OnMoveListener() {
@Override
public void onMove(int angle, int strength) {
mLastDirectionInt = mCurrentDirectionInt;
mCurrentDirectionInt = getDirectionInt(angle, strength);
if (mLastDirectionInt != mCurrentDirectionInt) {
sendDirectionalKeycode(mLastDirectionInt, false);
sendDirectionalKeycode(mCurrentDirectionInt, true);
}
}
@Override
public void onForwardLock(boolean isLocked) {
sendInput(mDirectionForwardLock, isLocked);
}
});
}
@Override
public View getControlView() {
return this;
}
@Override
public ControlData getProperties() {
return mControlData;
}
@Override
public void setProperties(ControlData properties, boolean changePos) {
mControlData = (ControlJoystickData) properties;
mControlData.isHideable = true;
ControlInterface.super.setProperties(properties, changePos);
postDelayed(() -> setForwardLockDistance(mControlData.forwardLock ? (int) Tools.dpToPx(60) : 0), 10);
}
@Override
public void removeButton() {
getControlLayoutParent().getLayout().mJoystickDataList.remove(getProperties());
getControlLayoutParent().removeView(this);
}
@Override
public void cloneButton() {
ControlData data = new ControlJoystickData(getProperties());
getControlLayoutParent().addJoystickButton((ControlJoystickData) data);
}
@Override
public void setBackground() {
setBorderWidth((int) Tools.dpToPx(getProperties().strokeWidth));
setBorderColor(getProperties().strokeColor);
setBackgroundColor(getProperties().bgColor);
}
@Override
public void sendKeyPresses(boolean isDown) {/*STUB since non swipeable*/ }
@Override
public void loadEditValues(EditControlPopup editControlPopup) {
editControlPopup.loadJoystickValues(mControlData);
}
private int getDirectionInt(int angle, int intensity) {
if (intensity == 0) return DIRECTION_NONE;
return (int) (((angle + 22.5) / 45) % 8);
}
private void sendDirectionalKeycode(int direction, boolean isDown) {
switch (direction) {
case DIRECTION_NORTH:
sendInput(mDirectionForward, isDown);
break;
case DIRECTION_NORTH_EAST:
sendInput(mDirectionForward, isDown);
sendInput(mDirectionRight, isDown);
break;
case DIRECTION_EAST:
sendInput(mDirectionRight, isDown);
break;
case DIRECTION_SOUTH_EAST:
sendInput(mDirectionRight, isDown);
sendInput(mDirectionBackward, isDown);
break;
case DIRECTION_SOUTH:
sendInput(mDirectionBackward, isDown);
break;
case DIRECTION_SOUTH_WEST:
sendInput(mDirectionBackward, isDown);
sendInput(mDirectionLeft, isDown);
break;
case DIRECTION_WEST:
sendInput(mDirectionLeft, isDown);
break;
case DIRECTION_NORTH_WEST:
sendInput(mDirectionForward, isDown);
sendInput(mDirectionLeft, isDown);
break;
case DIRECTION_FORWARD_LOCK:
sendInput(mDirectionForwardLock, isDown);
break;
}
}
}

View File

@@ -8,6 +8,7 @@ import android.view.ViewGroup;
import net.kdt.pojavlaunch.customcontrols.ControlData;
import net.kdt.pojavlaunch.customcontrols.ControlDrawerData;
import net.kdt.pojavlaunch.customcontrols.ControlLayout;
import net.kdt.pojavlaunch.customcontrols.handleview.EditControlPopup;
@SuppressLint("ViewConstructor")
public class ControlSubButton extends ControlButton {
@@ -21,8 +22,10 @@ public class ControlSubButton extends ControlButton {
}
private void filterProperties(){
mProperties.setHeight(parentDrawer.getProperties().getHeight());
mProperties.setWidth(parentDrawer.getProperties().getWidth());
if (parentDrawer != null && parentDrawer.drawerData.orientation != ControlDrawerData.Orientation.FREE) {
mProperties.setHeight(parentDrawer.getProperties().getHeight());
mProperties.setWidth(parentDrawer.getProperties().getWidth());
}
mProperties.isDynamicBtn = false;
setProperties(mProperties, false);
@@ -30,12 +33,18 @@ public class ControlSubButton extends ControlButton {
@Override
public void setVisible(boolean isVisible) {
setVisibility(isVisible ? (parentDrawer.areButtonsVisible ? VISIBLE : GONE) : (!mProperties.isHideable && parentDrawer.getVisibility() == GONE) ? VISIBLE : View.GONE);
// STUB, visibility handled by the ControlDrawer
//setVisibility(isVisible ? VISIBLE : (!mProperties.isHideable && parentDrawer.getVisibility() == GONE) ? VISIBLE : View.GONE);
}
@Override
public void onGrabState(boolean isGrabbing) {
// STUB, visibility lifecycle handled by the ControlDrawer
}
@Override
public void setLayoutParams(ViewGroup.LayoutParams params) {
if(parentDrawer != null){
if(parentDrawer != null && parentDrawer.drawerData.orientation != ControlDrawerData.Orientation.FREE){
params.width = (int)parentDrawer.mProperties.getWidth();
params.height = (int)parentDrawer.mProperties.getHeight();
}
@@ -81,4 +90,9 @@ public class ControlSubButton extends ControlButton {
super.snapAndAlign(x, y);
// Else the button is forced into place
}
@Override
public void loadEditValues(EditControlPopup editControlPopup) {
editControlPopup.loadSubButtonValues(getProperties(), parentDrawer.drawerData.orientation);
}
}

View File

@@ -19,6 +19,8 @@ import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.SeekBar;
import android.widget.Spinner;
@@ -34,6 +36,7 @@ import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.colorselector.ColorSelector;
import net.kdt.pojavlaunch.customcontrols.ControlData;
import net.kdt.pojavlaunch.customcontrols.ControlDrawerData;
import net.kdt.pojavlaunch.customcontrols.ControlJoystickData;
import net.kdt.pojavlaunch.customcontrols.buttons.ControlDrawer;
import net.kdt.pojavlaunch.customcontrols.buttons.ControlInterface;
@@ -43,54 +46,54 @@ import java.util.List;
* Class providing a sort of popup on top of a Layout, allowing to edit a given ControlButton
*/
public class EditControlPopup {
protected final Spinner[] mKeycodeSpinners = new Spinner[4];
private final DefocusableScrollView mScrollView;
private ConstraintLayout mRootView;
private final ColorSelector mColorSelector;
private final ObjectAnimator mEditPopupAnimator;
private final ObjectAnimator mColorEditorAnimator;
private boolean mDisplaying = false;
private boolean mDisplayingColor = false;
public boolean internalChanges = false; // True when we programmatically change stuff.
private ControlInterface mCurrentlyEditedButton;
private final int mMargin;
public boolean internalChanges = false; // True when we programmatically change stuff.
private final View.OnLayoutChangeListener mLayoutChangedListener = new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if(internalChanges) return;
if (internalChanges) return;
internalChanges = true;
int width = (int)(safeParseFloat(mWidthEditText.getText().toString()));
int width = (int) (safeParseFloat(mWidthEditText.getText().toString()));
if(width >= 0 && Math.abs(right - width) > 1){
if (width >= 0 && Math.abs(right - width) > 1) {
mWidthEditText.setText(String.valueOf(right - left));
}
int height = (int)(safeParseFloat(mHeightEditText.getText().toString()));
if(height >= 0 && Math.abs(bottom - height) > 1){
int height = (int) (safeParseFloat(mHeightEditText.getText().toString()));
if (height >= 0 && Math.abs(bottom - height) > 1) {
mHeightEditText.setText(String.valueOf(bottom - top));
}
internalChanges = false;
}
};
protected EditText mNameEditText, mWidthEditText, mHeightEditText;
@SuppressLint("UseSwitchCompatOrMaterialCode")
protected Switch mToggleSwitch, mPassthroughSwitch, mSwipeableSwitch;
protected Switch mToggleSwitch, mPassthroughSwitch, mSwipeableSwitch, mForwardLockSwitch;
protected Spinner mOrientationSpinner;
protected final Spinner[] mKeycodeSpinners = new Spinner[4];
protected TextView[] mKeycodeTextviews = new TextView[4];
protected SeekBar mStrokeWidthSeekbar, mCornerRadiusSeekbar, mAlphaSeekbar;
protected TextView mStrokePercentTextView, mCornerRadiusPercentTextView, mAlphaPercentTextView;
protected TextView mSelectBackgroundColor, mSelectStrokeColor;
protected ArrayAdapter<String> mAdapter;
protected List<String> mSpecialArray;
protected CheckBox mDisplayInGameCheckbox, mDisplayInMenuCheckbox;
private ConstraintLayout mRootView;
private boolean mDisplaying = false;
private boolean mDisplayingColor = false;
private ControlInterface mCurrentlyEditedButton;
// Decorative textviews
private TextView mOrientationTextView, mMappingTextView, mNameTextView, mCornerRadiusTextView;
private TextView mOrientationTextView, mMappingTextView, mNameTextView,
mCornerRadiusTextView, mVisibilityTextView, mSizeTextview, mSizeXTextView;
public EditControlPopup(Context context, ViewGroup parent){
public EditControlPopup(Context context, ViewGroup parent) {
mScrollView = (DefocusableScrollView) LayoutInflater.from(context).inflate(R.layout.dialog_control_button_setting, parent, false);
parent.addView(mScrollView);
@@ -101,8 +104,8 @@ public class EditControlPopup {
mColorSelector.getRootView().setTranslationZ(11);
mColorSelector.getRootView().setX(-context.getResources().getDimensionPixelOffset(R.dimen._280sdp));
mEditPopupAnimator = ObjectAnimator.ofFloat(mScrollView, "x", 0).setDuration(1000);
mColorEditorAnimator = ObjectAnimator.ofFloat(mColorSelector.getRootView(), "x", 0).setDuration(1000);
mEditPopupAnimator = ObjectAnimator.ofFloat(mScrollView, "x", 0).setDuration(600);
mColorEditorAnimator = ObjectAnimator.ofFloat(mColorSelector.getRootView(), "x", 0).setDuration(600);
Interpolator decelerate = new AccelerateDecelerateInterpolator();
mEditPopupAnimator.setInterpolator(decelerate);
mColorEditorAnimator.setInterpolator(decelerate);
@@ -117,18 +120,23 @@ public class EditControlPopup {
setupRealTimeListeners();
}
public static void setPercentageText(TextView textView, int progress) {
textView.setText(textView.getContext().getString(R.string.percent_format, progress));
}
/** Slide the layout into the visible screen area */
public void appear(boolean fromRight){
/**
* Slide the layout into the visible screen area
*/
public void appear(boolean fromRight) {
disappearColor(); // When someone jumps from a button to another
if(fromRight){
if(!mDisplaying || !isAtRight()){
if (fromRight) {
if (!mDisplaying || !isAtRight()) {
mEditPopupAnimator.setFloatValues(currentDisplayMetrics.widthPixels, currentDisplayMetrics.widthPixels - mScrollView.getWidth() - mMargin);
mEditPopupAnimator.start();
}
}else{
if (!mDisplaying || isAtRight()){
} else {
if (!mDisplaying || isAtRight()) {
mEditPopupAnimator.setFloatValues(-mScrollView.getWidth(), mMargin);
mEditPopupAnimator.start();
}
@@ -137,12 +145,14 @@ public class EditControlPopup {
mDisplaying = true;
}
/** Slide out the layout */
public void disappear(){
if(!mDisplaying) return;
/**
* Slide out the layout
*/
public void disappear() {
if (!mDisplaying) return;
mDisplaying = false;
if(isAtRight())
if (isAtRight())
mEditPopupAnimator.setFloatValues(currentDisplayMetrics.widthPixels - mScrollView.getWidth() - mMargin, currentDisplayMetrics.widthPixels);
else
mEditPopupAnimator.setFloatValues(mMargin, -mScrollView.getWidth());
@@ -150,30 +160,39 @@ public class EditControlPopup {
mEditPopupAnimator.start();
}
/** Slide the layout into the visible screen area */
public void appearColor(boolean fromRight, int color){
if(fromRight){
if(!mDisplayingColor || !isAtRight()){
/**
* Slide the layout into the visible screen area
*/
public void appearColor(boolean fromRight, int color) {
if (fromRight) {
if (!mDisplayingColor || !isAtRight()) {
mColorEditorAnimator.setFloatValues(currentDisplayMetrics.widthPixels, currentDisplayMetrics.widthPixels - mScrollView.getWidth() - mMargin);
mColorEditorAnimator.start();
}
}else{
if (!mDisplayingColor || isAtRight()){
} else {
if (!mDisplayingColor || isAtRight()) {
mColorEditorAnimator.setFloatValues(-mScrollView.getWidth(), mMargin);
mColorEditorAnimator.start();
}
}
// Adjust the color selector to have the same size as the control settings
ViewGroup.LayoutParams params = mColorSelector.getRootView().getLayoutParams();
params.height = mScrollView.getHeight();
mColorSelector.getRootView().setLayoutParams(params);
mDisplayingColor = true;
mColorSelector.show(color == -1 ? Color.WHITE : color);
}
/** Slide out the layout */
public void disappearColor(){
if(!mDisplayingColor) return;
/**
* Slide out the layout
*/
public void disappearColor() {
if (!mDisplayingColor) return;
mDisplayingColor = false;
if(isAtRight())
if (isAtRight())
mColorEditorAnimator.setFloatValues(currentDisplayMetrics.widthPixels - mScrollView.getWidth() - mMargin, currentDisplayMetrics.widthPixels);
else
mColorEditorAnimator.setFloatValues(mMargin, -mScrollView.getWidth());
@@ -181,33 +200,37 @@ public class EditControlPopup {
mColorEditorAnimator.start();
}
/** Slide out the first visible layer.
* @return True if the last layer is disappearing */
public boolean disappearLayer(){
if(mDisplayingColor){
/**
* Slide out the first visible layer.
*
* @return True if the last layer is disappearing
*/
public boolean disappearLayer() {
if (mDisplayingColor) {
disappearColor();
return false;
}else{
} else {
disappear();
return true;
}
}
/** Switch the panels position if needed */
public void adaptPanelPosition(){
if(mDisplaying){
boolean isAtRight = mCurrentlyEditedButton.getControlView().getX() + mCurrentlyEditedButton.getControlView().getWidth()/2f < currentDisplayMetrics.widthPixels/2f;
/**
* Switch the panels position if needed
*/
public void adaptPanelPosition() {
if (mDisplaying) {
boolean isAtRight = mCurrentlyEditedButton.getControlView().getX() + mCurrentlyEditedButton.getControlView().getWidth() / 2f < currentDisplayMetrics.widthPixels / 2f;
appear(isAtRight);
}
}
public void destroy(){
public void destroy() {
((ViewGroup) mScrollView.getParent()).removeView(mColorSelector.getRootView());
((ViewGroup) mScrollView.getParent()).removeView(mScrollView);
}
private void loadAdapter(){
private void loadAdapter() {
//Initialize adapter for keycodes
mAdapter = new ArrayAdapter<>(mRootView.getContext(), R.layout.item_centered_textview);
mSpecialArray = ControlData.buildSpecialButtonArray();
@@ -228,48 +251,50 @@ public class EditControlPopup {
mOrientationSpinner.setAdapter(adapter);
}
private void setDefaultVisibilitySetting(){
for(int i=0; i<mRootView.getChildCount(); ++i){
private void setDefaultVisibilitySetting() {
for (int i = 0; i < mRootView.getChildCount(); ++i) {
mRootView.getChildAt(i).setVisibility(VISIBLE);
}
for(Spinner s : mKeycodeSpinners) {
s.setVisibility(View.INVISIBLE);
}
}
private boolean isAtRight(){
return mScrollView.getX() > currentDisplayMetrics.widthPixels/2f;
}
public static void setPercentageText(TextView textView, int progress){
textView.setText(textView.getContext().getString(R.string.percent_format, progress));
private boolean isAtRight() {
return mScrollView.getX() > currentDisplayMetrics.widthPixels / 2f;
}
/* LOADING VALUES */
/** Load values for basic control data */
public void loadValues(ControlData data){
/**
* Load values for basic control data
*/
public void loadValues(ControlData data) {
setDefaultVisibilitySetting();
mOrientationTextView.setVisibility(GONE);
mOrientationSpinner.setVisibility(GONE);
mForwardLockSwitch.setVisibility(GONE);
mNameEditText.setText(data.name);
mWidthEditText.setText(String.valueOf(data.getWidth()));
mHeightEditText.setText(String.valueOf(data.getHeight()));
mAlphaSeekbar.setProgress((int) (data.opacity*100));
mStrokeWidthSeekbar.setProgress(data.strokeWidth);
mAlphaSeekbar.setProgress((int) (data.opacity * 100));
mStrokeWidthSeekbar.setProgress((int) data.strokeWidth * 10);
mCornerRadiusSeekbar.setProgress((int) data.cornerRadius);
setPercentageText(mAlphaPercentTextView, (int) (data.opacity*100));
setPercentageText(mStrokePercentTextView, data.strokeWidth);
setPercentageText(mAlphaPercentTextView, (int) (data.opacity * 100));
setPercentageText(mStrokePercentTextView, (int) data.strokeWidth * 10);
setPercentageText(mCornerRadiusPercentTextView, (int) data.cornerRadius);
mToggleSwitch.setChecked(data.isToggle);
mPassthroughSwitch.setChecked(data.passThruEnabled);
mSwipeableSwitch.setChecked(data.isSwipeable);
for(int i = 0; i< data.keycodes.length; i++){
mDisplayInGameCheckbox.setChecked(data.displayInGame);
mDisplayInMenuCheckbox.setChecked(data.displayInMenu);
for (int i = 0; i < data.keycodes.length; i++) {
if (data.keycodes[i] < 0) {
mKeycodeSpinners[i].setSelection(data.keycodes[i] + mSpecialArray.size());
} else {
@@ -278,18 +303,20 @@ public class EditControlPopup {
}
}
/** Load values for extended control data */
public void loadValues(ControlDrawerData data){
/**
* Load values for extended control data
*/
public void loadValues(ControlDrawerData data) {
loadValues(data.properties);
mOrientationSpinner.setSelection(
ControlDrawerData.orientationToInt(data.orientation));
mMappingTextView.setVisibility(GONE);
mKeycodeSpinners[0].setVisibility(GONE);
mKeycodeSpinners[1].setVisibility(GONE);
mKeycodeSpinners[2].setVisibility(GONE);
mKeycodeSpinners[3].setVisibility(GONE);
for (int i = 0; i < mKeycodeSpinners.length; i++) {
mKeycodeSpinners[i].setVisibility(GONE);
mKeycodeTextviews[i].setVisibility(GONE);
}
mOrientationTextView.setVisibility(VISIBLE);
mOrientationSpinner.setVisibility(VISIBLE);
@@ -299,15 +326,17 @@ public class EditControlPopup {
mToggleSwitch.setVisibility(View.GONE);
}
/** Load values for the joystick */
@SuppressWarnings("unused") public void loadJoystickValues(ControlData data){
/**
* Load values for the joystick
*/
public void loadJoystickValues(ControlJoystickData data) {
loadValues(data);
mMappingTextView.setVisibility(GONE);
mKeycodeSpinners[0].setVisibility(GONE);
mKeycodeSpinners[1].setVisibility(GONE);
mKeycodeSpinners[2].setVisibility(GONE);
mKeycodeSpinners[3].setVisibility(GONE);
for (int i = 0; i < mKeycodeSpinners.length; i++) {
mKeycodeSpinners[i].setVisibility(GONE);
mKeycodeTextviews[i].setVisibility(GONE);
}
mNameTextView.setVisibility(GONE);
mNameEditText.setVisibility(GONE);
@@ -319,10 +348,33 @@ public class EditControlPopup {
mSwipeableSwitch.setVisibility(View.GONE);
mPassthroughSwitch.setVisibility(View.GONE);
mToggleSwitch.setVisibility(View.GONE);
mForwardLockSwitch.setVisibility(VISIBLE);
mForwardLockSwitch.setChecked(data.forwardLock);
}
/**
* Load values for sub buttons
*/
public void loadSubButtonValues(ControlData data, ControlDrawerData.Orientation drawerOrientation) {
loadValues(data);
// Size linked to the parent drawer depending on the drawer settings
if(drawerOrientation != ControlDrawerData.Orientation.FREE){
mSizeTextview.setVisibility(GONE);
mSizeXTextView.setVisibility(GONE);
mWidthEditText.setVisibility(GONE);
mHeightEditText.setVisibility(GONE);
}
// No conditional, already depends on the parent drawer visibility
mVisibilityTextView.setVisibility(GONE);
mDisplayInMenuCheckbox.setVisibility(GONE);
mDisplayInGameCheckbox.setVisibility(GONE);
}
private void bindLayout(){
private void bindLayout() {
mRootView = mScrollView.findViewById(R.id.edit_layout);
mNameEditText = mScrollView.findViewById(R.id.editName_editText);
mWidthEditText = mScrollView.findViewById(R.id.editSize_editTextX);
@@ -330,10 +382,15 @@ public class EditControlPopup {
mToggleSwitch = mScrollView.findViewById(R.id.checkboxToggle);
mPassthroughSwitch = mScrollView.findViewById(R.id.checkboxPassThrough);
mSwipeableSwitch = mScrollView.findViewById(R.id.checkboxSwipeable);
mForwardLockSwitch = mScrollView.findViewById(R.id.checkboxForwardLock);
mKeycodeSpinners[0] = mScrollView.findViewById(R.id.editMapping_spinner_1);
mKeycodeSpinners[1] = mScrollView.findViewById(R.id.editMapping_spinner_2);
mKeycodeSpinners[2] = mScrollView.findViewById(R.id.editMapping_spinner_3);
mKeycodeSpinners[3] = mScrollView.findViewById(R.id.editMapping_spinner_4);
mKeycodeTextviews[0] = mScrollView.findViewById(R.id.mapping_1_textview);
mKeycodeTextviews[1] = mScrollView.findViewById(R.id.mapping_2_textview);
mKeycodeTextviews[2] = mScrollView.findViewById(R.id.mapping_3_textview);
mKeycodeTextviews[3] = mScrollView.findViewById(R.id.mapping_4_textview);
mOrientationSpinner = mScrollView.findViewById(R.id.editOrientation_spinner);
mStrokeWidthSeekbar = mScrollView.findViewById(R.id.editStrokeWidth_seekbar);
mCornerRadiusSeekbar = mScrollView.findViewById(R.id.editCornerRadius_seekbar);
@@ -343,28 +400,36 @@ public class EditControlPopup {
mStrokePercentTextView = mScrollView.findViewById(R.id.editStrokeWidth_textView_percent);
mAlphaPercentTextView = mScrollView.findViewById(R.id.editButtonOpacity_textView_percent);
mCornerRadiusPercentTextView = mScrollView.findViewById(R.id.editCornerRadius_textView_percent);
mDisplayInGameCheckbox = mScrollView.findViewById(R.id.visibility_game_checkbox);
mDisplayInMenuCheckbox = mScrollView.findViewById(R.id.visibility_menu_checkbox);
//Decorative stuff
mMappingTextView = mScrollView.findViewById(R.id.editMapping_textView);
mOrientationTextView = mScrollView.findViewById(R.id.editOrientation_textView);
mNameTextView = mScrollView.findViewById(R.id.editName_textView);
mCornerRadiusTextView = mScrollView.findViewById(R.id.editCornerRadius_textView);
mVisibilityTextView = mScrollView.findViewById(R.id.visibility_textview);
mSizeTextview = mScrollView.findViewById(R.id.editSize_textView);
mSizeXTextView = mScrollView.findViewById(R.id.editSize_x_textView);
}
/**
* A long function linking all the displayed data on the popup and,
* the currently edited mCurrentlyEditedButton
*/
public void setupRealTimeListeners(){
public void setupRealTimeListeners() {
mNameEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if(internalChanges) return;
if (internalChanges) return;
mCurrentlyEditedButton.getProperties().name = s.toString();
@@ -375,16 +440,19 @@ public class EditControlPopup {
mWidthEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if(internalChanges) return;
if (internalChanges) return;
float width = safeParseFloat(s.toString());
if(width >= 0){
if (width >= 0) {
mCurrentlyEditedButton.getProperties().setWidth(width);
mCurrentlyEditedButton.updateProperties();
}
@@ -393,16 +461,19 @@ public class EditControlPopup {
mHeightEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if(internalChanges) return;
if (internalChanges) return;
float height = safeParseFloat(s.toString());
if(height >= 0){
if (height >= 0) {
mCurrentlyEditedButton.getProperties().setHeight(height);
mCurrentlyEditedButton.updateProperties();
}
@@ -410,66 +481,83 @@ public class EditControlPopup {
});
mSwipeableSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if(internalChanges) return;
if (internalChanges) return;
mCurrentlyEditedButton.getProperties().isSwipeable = isChecked;
});
mToggleSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if(internalChanges) return;
if (internalChanges) return;
mCurrentlyEditedButton.getProperties().isToggle = isChecked;
});
mPassthroughSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if(internalChanges) return;
if (internalChanges) return;
mCurrentlyEditedButton.getProperties().passThruEnabled = isChecked;
});
mForwardLockSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (internalChanges) return;
if(mCurrentlyEditedButton.getProperties() instanceof ControlJoystickData){
((ControlJoystickData) mCurrentlyEditedButton.getProperties()).forwardLock = isChecked;
}
});
mAlphaSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if(internalChanges) return;
mCurrentlyEditedButton.getProperties().opacity = mAlphaSeekbar.getProgress()/100f;
mCurrentlyEditedButton.getControlView().setAlpha(mAlphaSeekbar.getProgress()/100f);
if (internalChanges) return;
mCurrentlyEditedButton.getProperties().opacity = mAlphaSeekbar.getProgress() / 100f;
mCurrentlyEditedButton.getControlView().setAlpha(mAlphaSeekbar.getProgress() / 100f);
setPercentageText(mAlphaPercentTextView, progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
mStrokeWidthSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if(internalChanges) return;
mCurrentlyEditedButton.getProperties().strokeWidth = mStrokeWidthSeekbar.getProgress();
if (internalChanges) return;
mCurrentlyEditedButton.getProperties().strokeWidth = mStrokeWidthSeekbar.getProgress() / 10F;
mCurrentlyEditedButton.setBackground();
setPercentageText(mStrokePercentTextView, progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
mCornerRadiusSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if(internalChanges) return;
if (internalChanges) return;
mCurrentlyEditedButton.getProperties().cornerRadius = mCornerRadiusSeekbar.getProgress();
mCurrentlyEditedButton.setBackground();
setPercentageText(mCornerRadiusPercentTextView, progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
for(int i = 0; i< mKeycodeSpinners.length; ++i){
for (int i = 0; i < mKeycodeSpinners.length; ++i) {
int finalI = i;
mKeycodeTextviews[i].setOnClickListener(v -> mKeycodeSpinners[finalI].performClick());
mKeycodeSpinners[i].setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
@@ -480,10 +568,12 @@ public class EditControlPopup {
} else {
mCurrentlyEditedButton.getProperties().keycodes[finalI] = EfficientAndroidLWJGLKeycode.getValueByIndex(mKeycodeSpinners[finalI].getSelectedItemPosition() - mSpecialArray.size());
}
mKeycodeTextviews[finalI].setText((String) mKeycodeSpinners[finalI].getSelectedItem());
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
public void onNothingSelected(AdapterView<?> parent) {
}
});
}
@@ -494,16 +584,26 @@ public class EditControlPopup {
// Side note, spinner listeners are fired later than all the other ones.
// Meaning the internalChanges bool is useless here.
if(mCurrentlyEditedButton instanceof ControlDrawer){
((ControlDrawer)mCurrentlyEditedButton).drawerData.orientation = ControlDrawerData.intToOrientation(mOrientationSpinner.getSelectedItemPosition());
((ControlDrawer)mCurrentlyEditedButton).syncButtons();
if (mCurrentlyEditedButton instanceof ControlDrawer) {
((ControlDrawer) mCurrentlyEditedButton).drawerData.orientation = ControlDrawerData.intToOrientation(mOrientationSpinner.getSelectedItemPosition());
((ControlDrawer) mCurrentlyEditedButton).syncButtons();
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
public void onNothingSelected(AdapterView<?> parent) {
}
});
mDisplayInGameCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (internalChanges) return;
mCurrentlyEditedButton.getProperties().displayInGame = isChecked;
});
mDisplayInMenuCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (internalChanges) return;
mCurrentlyEditedButton.getProperties().displayInMenu = isChecked;
});
mSelectStrokeColor.setOnClickListener(v -> {
mColorSelector.setAlphaEnabled(false);
@@ -524,18 +624,18 @@ public class EditControlPopup {
});
}
private float safeParseFloat(String string){
private float safeParseFloat(String string) {
float out = -1; // -1
try {
out = Float.parseFloat(string);
}catch (NumberFormatException e){
} catch (NumberFormatException e) {
Log.e("EditControlPopup", e.toString());
}
return out;
}
public void setCurrentlyEditedButton(ControlInterface button){
if(mCurrentlyEditedButton != null)
public void setCurrentlyEditedButton(ControlInterface button) {
if (mCurrentlyEditedButton != null)
mCurrentlyEditedButton.getControlView().removeOnLayoutChangeListener(mLayoutChangedListener);
mCurrentlyEditedButton = button;
mCurrentlyEditedButton.getControlView().addOnLayoutChangeListener(mLayoutChangedListener);

View File

@@ -1,195 +1,24 @@
package net.kdt.pojavlaunch.fragments;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import net.kdt.pojavlaunch.JavaGUILauncherActivity;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.modloaders.FabricDownloadTask;
import net.kdt.pojavlaunch.modloaders.FabricUtils;
import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener;
import net.kdt.pojavlaunch.modloaders.FabriclikeUtils;
import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy;
import net.kdt.pojavlaunch.profiles.VersionSelectorDialog;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class FabricInstallFragment extends FabriclikeInstallFragment {
public class FabricInstallFragment extends Fragment implements AdapterView.OnItemSelectedListener, ModloaderDownloadListener, Runnable {
public static final String TAG = "FabricInstallTarget";
public static final String TAG = "FabricInstallFragment";
private static ModloaderListenerProxy sTaskProxy;
private TextView mSelectedVersionLabel;
private String mSelectedLoaderVersion;
private Spinner mLoaderVersionSpinner;
private String mSelectedGameVersion;
private boolean mSelectedSnapshot;
private ProgressBar mProgressBar;
private File mDestinationDir;
private Button mStartButton;
private View mRetryView;
public FabricInstallFragment() {
super(R.layout.fragment_fabric_install);
super(FabriclikeUtils.FABRIC_UTILS);
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
this.mDestinationDir = new File(Tools.DIR_CACHE, "fabric-installer");
protected ModloaderListenerProxy getListenerProxy() {
return sTaskProxy;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mStartButton = view.findViewById(R.id.fabric_installer_start_button);
mStartButton.setOnClickListener(this::onClickStart);
mSelectedVersionLabel = view.findViewById(R.id.fabric_installer_version_select_label);
view.findViewById(R.id.fabric_installer_game_version_change).setOnClickListener(this::onClickSelect);
mLoaderVersionSpinner = view.findViewById(R.id.fabric_installer_loader_ver_spinner);
mLoaderVersionSpinner.setOnItemSelectedListener(this);
mProgressBar = view.findViewById(R.id.fabric_installer_progress_bar);
mRetryView = view.findViewById(R.id.fabric_installer_retry_layout);
view.findViewById(R.id.fabric_installer_retry_button).setOnClickListener(this::onClickRetry);
if(sTaskProxy != null) {
mStartButton.setEnabled(false);
sTaskProxy.attachListener(this);
}
new Thread(this).start();
}
@Override
public void onDestroyView() {
super.onDestroyView();
if(sTaskProxy != null) {
sTaskProxy.detachListener();
}
}
private void onClickStart(View v) {
if(ProgressKeeper.hasOngoingTasks()) {
Toast.makeText(v.getContext(), R.string.tasks_ongoing, Toast.LENGTH_LONG).show();
return;
}
sTaskProxy = new ModloaderListenerProxy();
FabricDownloadTask fabricDownloadTask = new FabricDownloadTask(sTaskProxy, mDestinationDir);
sTaskProxy.attachListener(this);
mStartButton.setEnabled(false);
new Thread(fabricDownloadTask).start();
}
private void onClickSelect(View v) {
VersionSelectorDialog.open(v.getContext(), true, (id, snapshot)->{
mSelectedGameVersion = id;
mSelectedVersionLabel.setText(mSelectedGameVersion);
mSelectedSnapshot = snapshot;
if(mSelectedLoaderVersion != null && sTaskProxy == null) mStartButton.setEnabled(true);
});
}
private void onClickRetry(View v) {
mLoaderVersionSpinner.setAdapter(null);
mStartButton.setEnabled(false);
mProgressBar.setVisibility(View.VISIBLE);
mRetryView.setVisibility(View.GONE);
new Thread(this).start();
}
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
Adapter adapter = adapterView.getAdapter();
mSelectedLoaderVersion = (String) adapter.getItem(i);
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
mSelectedLoaderVersion = null;
}
@Override
public void onDownloadFinished(File downloadedFile) {
Tools.runOnUiThread(()->{
Context context = requireContext();
sTaskProxy.detachListener();
sTaskProxy = null;
mStartButton.setEnabled(true);
// This works because the due to the fact that we have transitioned here
// without adding a transaction to the back stack, which caused the previous
// transaction to be amended (i guess?? thats how the back stack dump looks like)
// we can get back to the main fragment with just one back stack pop.
// For some reason that amendment causes the transaction to lose its tag
// so we cant use the tag here.
getParentFragmentManager().popBackStackImmediate();
Intent intent = new Intent(context, JavaGUILauncherActivity.class);
FabricUtils.addAutoInstallArgs(intent, downloadedFile, mSelectedGameVersion, mSelectedLoaderVersion, mSelectedSnapshot, true);
context.startActivity(intent);
});
}
@Override
public void onDataNotAvailable() {
Tools.runOnUiThread(()->{
Context context = requireContext();
sTaskProxy.detachListener();
sTaskProxy = null;
mStartButton.setEnabled(true);
Tools.dialog(context,
context.getString(R.string.global_error),
context.getString(R.string.fabric_dl_cant_read_meta));
});
}
@Override
public void onDownloadError(Exception e) {
Tools.runOnUiThread(()-> {
Context context = requireContext();
sTaskProxy.detachListener();
sTaskProxy = null;
mStartButton.setEnabled(true);
Tools.showError(context, e);
});
}
@Override
public void run() {
try {
List<String> mLoaderVersions = FabricUtils.downloadLoaderVersionList(false);
if (mLoaderVersions != null) {
Tools.runOnUiThread(()->{
Context context = getContext();
if(context == null) return;
ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(context, R.layout.support_simple_spinner_dropdown_item, mLoaderVersions);
mLoaderVersionSpinner.setAdapter(arrayAdapter);
mProgressBar.setVisibility(View.GONE);
});
}else{
Tools.runOnUiThread(()-> {
mRetryView.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.GONE);
});
}
}catch (IOException e) {
Tools.runOnUiThread(()-> {
if(getContext() != null) Tools.showError(getContext(), e);
mRetryView.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.GONE);
});
}
protected void setListenerProxy(ModloaderListenerProxy listenerProxy) {
sTaskProxy = listenerProxy;
}
}

View File

@@ -0,0 +1,293 @@
package net.kdt.pojavlaunch.fragments;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import net.kdt.pojavlaunch.PojavApplication;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.modloaders.FabriclikeDownloadTask;
import net.kdt.pojavlaunch.modloaders.FabriclikeUtils;
import net.kdt.pojavlaunch.modloaders.FabricVersion;
import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener;
import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy;
import net.kdt.pojavlaunch.modloaders.modpacks.SelfReferencingFuture;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.Future;
public abstract class FabriclikeInstallFragment extends Fragment implements ModloaderDownloadListener, CompoundButton.OnCheckedChangeListener {
private final FabriclikeUtils mFabriclikeUtils;
private Spinner mGameVersionSpinner;
private FabricVersion[] mGameVersionArray;
private Future<?> mGameVersionFuture;
private String mSelectedGameVersion;
private Spinner mLoaderVersionSpinner;
private FabricVersion[] mLoaderVersionArray;
private Future<?> mLoaderVersionFuture;
private String mSelectedLoaderVersion;
private ProgressBar mProgressBar;
private Button mStartButton;
private View mRetryView;
private CheckBox mOnlyStableCheckbox;
protected FabriclikeInstallFragment(FabriclikeUtils mFabriclikeUtils) {
super(R.layout.fragment_fabric_install);
this.mFabriclikeUtils = mFabriclikeUtils;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mStartButton = view.findViewById(R.id.fabric_installer_start_button);
mStartButton.setOnClickListener(this::onClickStart);
mGameVersionSpinner = view.findViewById(R.id.fabric_installer_game_ver_spinner);
mGameVersionSpinner.setOnItemSelectedListener(new GameVersionSelectedListener());
mLoaderVersionSpinner = view.findViewById(R.id.fabric_installer_loader_ver_spinner);
mLoaderVersionSpinner.setOnItemSelectedListener(new LoaderVersionSelectedListener());
mProgressBar = view.findViewById(R.id.fabric_installer_progress_bar);
mRetryView = view.findViewById(R.id.fabric_installer_retry_layout);
mOnlyStableCheckbox = view.findViewById(R.id.fabric_installer_only_stable_checkbox);
mOnlyStableCheckbox.setOnCheckedChangeListener(this);
view.findViewById(R.id.fabric_installer_retry_button).setOnClickListener(this::onClickRetry);
((TextView)view.findViewById(R.id.fabric_installer_label_loader_ver)).setText(getString(R.string.fabric_dl_loader_version, mFabriclikeUtils.getName()));
ModloaderListenerProxy proxy = getListenerProxy();
if(proxy != null) {
mStartButton.setEnabled(false);
proxy.attachListener(this);
}
updateGameVersions();
}
@Override
public void onStop() {
cancelFutureChecked(mGameVersionFuture);
cancelFutureChecked(mLoaderVersionFuture);
ModloaderListenerProxy proxy = getListenerProxy();
if(proxy != null) {
proxy.detachListener();
}
super.onStop();
}
private void onClickStart(View v) {
if(ProgressKeeper.hasOngoingTasks()) {
Toast.makeText(v.getContext(), R.string.tasks_ongoing, Toast.LENGTH_LONG).show();
return;
}
ModloaderListenerProxy proxy = new ModloaderListenerProxy();
FabriclikeDownloadTask fabricDownloadTask = new FabriclikeDownloadTask(proxy, mFabriclikeUtils,
mSelectedGameVersion, mSelectedLoaderVersion, true);
proxy.attachListener(this);
setListenerProxy(proxy);
mStartButton.setEnabled(false);
new Thread(fabricDownloadTask).start();
}
private void onClickRetry(View v) {
mStartButton.setEnabled(false);
mRetryView.setVisibility(View.GONE);
mLoaderVersionSpinner.setAdapter(null);
if(mGameVersionArray == null) {
mGameVersionSpinner.setAdapter(null);
updateGameVersions();
return;
}
updateLoaderVersions();
}
@Override
public void onDownloadFinished(File downloadedFile) {
Tools.runOnUiThread(()->{
getListenerProxy().detachListener();
setListenerProxy(null);
mStartButton.setEnabled(true);
// This works because the due to the fact that we have transitioned here
// without adding a transaction to the back stack, which caused the previous
// transaction to be amended (i guess?? thats how the back stack dump looks like)
// we can get back to the main fragment with just one back stack pop.
// For some reason that amendment causes the transaction to lose its tag
// so we cant use the tag here.
getParentFragmentManager().popBackStackImmediate();
});
}
@Override
public void onDataNotAvailable() {
Tools.runOnUiThread(()->{
Context context = requireContext();
getListenerProxy().detachListener();
setListenerProxy(null);
mStartButton.setEnabled(true);
Tools.dialog(context,
context.getString(R.string.global_error),
context.getString(R.string.fabric_dl_cant_read_meta, mFabriclikeUtils.getName()));
});
}
@Override
public void onDownloadError(Exception e) {
Tools.runOnUiThread(()-> {
Context context = requireContext();
getListenerProxy().detachListener();
setListenerProxy(null);
mStartButton.setEnabled(true);
Tools.showError(context, e);
});
}
private void cancelFutureChecked(Future<?> future) {
if(future != null && !future.isCancelled()) future.cancel(true);
}
private void startLoading() {
mProgressBar.setVisibility(View.VISIBLE);
mStartButton.setEnabled(false);
}
private void stopLoading() {
mProgressBar.setVisibility(View.GONE);
// The "visibility on" is managed by the spinners
}
private ArrayAdapter<FabricVersion> createAdapter(FabricVersion[] fabricVersions, boolean onlyStable) {
ArrayList<FabricVersion> filteredVersions = new ArrayList<>(fabricVersions.length);
for(FabricVersion fabricVersion : fabricVersions) {
if(!onlyStable || fabricVersion.stable) filteredVersions.add(fabricVersion);
}
filteredVersions.trimToSize();
return new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_dropdown_item, filteredVersions);
}
private void onException(Future<?> myFuture, Exception e) {
Tools.runOnUiThread(()->{
if(myFuture.isCancelled()) return;
stopLoading();
if(e != null) Tools.showError(requireContext(), e);
mRetryView.setVisibility(View.VISIBLE);
});
}
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
updateGameSpinner();
updateLoaderSpinner();
}
class LoaderVersionSelectedListener implements AdapterView.OnItemSelectedListener {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
mSelectedLoaderVersion = ((FabricVersion) adapterView.getAdapter().getItem(i)).version;
mStartButton.setEnabled(mSelectedGameVersion != null);
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
mSelectedLoaderVersion = null;
mStartButton.setEnabled(false);
}
}
class LoadLoaderVersionsTask implements SelfReferencingFuture.FutureInterface {
@Override
public void run(Future<?> myFuture) {
Log.i("LoadLoaderVersions", "Starting...");
try {
mLoaderVersionArray = mFabriclikeUtils.downloadLoaderVersions(mSelectedGameVersion);
if(mLoaderVersionArray != null) onFinished(myFuture);
else onException(myFuture, null);
}catch (IOException e) {
onException(myFuture, e);
}
}
private void onFinished(Future<?> myFuture) {
Tools.runOnUiThread(()->{
if(myFuture.isCancelled()) return;
stopLoading();
updateLoaderSpinner();
});
}
}
private void updateLoaderVersions() {
startLoading();
mLoaderVersionFuture = new SelfReferencingFuture(new LoadLoaderVersionsTask()).startOnExecutor(PojavApplication.sExecutorService);
}
private void updateLoaderSpinner() {
mLoaderVersionSpinner.setAdapter(createAdapter(mLoaderVersionArray, mOnlyStableCheckbox.isChecked()));
}
class GameVersionSelectedListener implements AdapterView.OnItemSelectedListener {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
mSelectedGameVersion = ((FabricVersion) adapterView.getAdapter().getItem(i)).version;
cancelFutureChecked(mLoaderVersionFuture);
updateLoaderVersions();
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
mSelectedGameVersion = null;
if(mLoaderVersionFuture != null) mLoaderVersionFuture.cancel(true);
adapterView.setAdapter(null);
}
}
class LoadGameVersionsTask implements SelfReferencingFuture.FutureInterface {
@Override
public void run(Future<?> myFuture) {
try {
mGameVersionArray = mFabriclikeUtils.downloadGameVersions();
if(mGameVersionArray != null) onFinished(myFuture);
else onException(myFuture, null);
}catch (IOException e) {
onException(myFuture, e);
}
}
private void onFinished(Future<?> myFuture) {
Tools.runOnUiThread(()->{
if(myFuture.isCancelled()) return;
stopLoading();
updateGameSpinner();
});
}
}
private void updateGameVersions() {
startLoading();
mGameVersionFuture = new SelfReferencingFuture(new LoadGameVersionsTask()).startOnExecutor(PojavApplication.sExecutorService);
}
private void updateGameSpinner() {
mGameVersionSpinner.setAdapter(createAdapter(mGameVersionArray, mOnlyStableCheckbox.isChecked()));
}
protected abstract ModloaderListenerProxy getListenerProxy();
protected abstract void setListenerProxy(ModloaderListenerProxy listenerProxy);
}

View File

@@ -59,7 +59,7 @@ public class ForgeInstallFragment extends ModVersionListFragment<List<String>> {
@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

View File

@@ -13,16 +13,21 @@ import androidx.annotation.NonNull;
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 class MainMenuFragment extends Fragment {
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 class MainMenuFragment extends Fragment {
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);

View File

@@ -58,10 +58,10 @@ public abstract class ModVersionListFragment<T> extends Fragment implements Runn
}
@Override
public void onDestroyView() {
public void onStop() {
ModloaderListenerProxy taskProxy = getTaskProxy();
if(taskProxy != null) taskProxy.detachListener();
super.onDestroyView();
super.onStop();
}
@Override

View File

@@ -51,8 +51,7 @@ public class OptiFineInstallFragment extends ModVersionListFragment<OptiFineUtil
@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

View File

@@ -31,7 +31,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public class ProfileEditorFragment extends Fragment {
public static final String TAG = "ProfileEditorFragment";
@@ -75,7 +74,7 @@ public class ProfileEditorFragment extends Fragment {
List<String> renderList = new ArrayList<>(5);
Collections.addAll(renderList, getResources().getStringArray(R.array.renderer));
renderList.add("Default");
mDefaultRenderer.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, renderList));
mDefaultRenderer.setAdapter(new ArrayAdapter<>(getContext(), R.layout.item_simple_list_1, renderList));
// Set up behaviors
mSaveButton.setOnClickListener(v -> {
@@ -84,9 +83,12 @@ public class ProfileEditorFragment extends Fragment {
});
mDeleteButton.setOnClickListener(v -> {
LauncherProfiles.mainProfileJson.profiles.remove(mProfileKey);
LauncherProfiles.update();
ExtraCore.setValue(ExtraConstants.REFRESH_VERSION_SPINNER, DELETED_PROFILE);
if(LauncherProfiles.mainProfileJson.profiles.size() > 1){
LauncherProfiles.mainProfileJson.profiles.remove(mProfileKey);
LauncherProfiles.write();
ExtraCore.setValue(ExtraConstants.REFRESH_VERSION_SPINNER, DELETED_PROFILE);
}
Tools.removeCurrentFragment(requireActivity());
});
@@ -160,11 +162,7 @@ public class ProfileEditorFragment extends Fragment {
mProfileKey = profile;
}else{
minecraftProfile = MinecraftProfile.createTemplate();
String uuid = UUID.randomUUID().toString();
while(LauncherProfiles.mainProfileJson.profiles.containsKey(uuid)) {
uuid = UUID.randomUUID().toString();
}
mProfileKey = uuid;
mProfileKey = LauncherProfiles.getFreeProfileKey();
}
return minecraftProfile;
}
@@ -208,7 +206,7 @@ public class ProfileEditorFragment extends Fragment {
LauncherProfiles.mainProfileJson.profiles.put(mProfileKey, mTempProfile);
LauncherProfiles.update();
LauncherProfiles.write();
ExtraCore.setValue(ExtraConstants.REFRESH_VERSION_SPINNER, mProfileKey);
}
}

View File

@@ -31,5 +31,9 @@ public class ProfileTypeSelectFragment extends Fragment {
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));
view.findViewById(R.id.modded_profile_quilt).setOnClickListener((v)->
Tools.swapFragment(requireActivity(), QuiltInstallFragment.class, QuiltInstallFragment.TAG, false, null));
}
}

View File

@@ -0,0 +1,24 @@
package net.kdt.pojavlaunch.fragments;
import net.kdt.pojavlaunch.modloaders.FabriclikeUtils;
import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy;
public class QuiltInstallFragment extends FabriclikeInstallFragment {
public static final String TAG = "QuiltInstallFragment";
private static ModloaderListenerProxy sTaskProxy;
public QuiltInstallFragment() {
super(FabriclikeUtils.QUILT_UTILS);
}
@Override
protected ModloaderListenerProxy getListenerProxy() {
return sTaskProxy;
}
@Override
protected void setListenerProxy(ModloaderListenerProxy listenerProxy) {
sTaskProxy = listenerProxy;
}
}

View File

@@ -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();
}
}

View File

@@ -1,77 +0,0 @@
package net.kdt.pojavlaunch.modloaders;
import com.kdt.mcgui.ProgressLayout;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import net.kdt.pojavlaunch.utils.DownloadUtils;
import java.io.File;
import java.io.IOException;
public class FabricDownloadTask implements Runnable, Tools.DownloaderFeedback{
private final File mDestinationDir;
private final File mDestinationFile;
private final ModloaderDownloadListener mModloaderDownloadListener;
public FabricDownloadTask(ModloaderDownloadListener modloaderDownloadListener, File mDestinationDir) {
this.mModloaderDownloadListener = modloaderDownloadListener;
this.mDestinationDir = mDestinationDir;
this.mDestinationFile = new File(mDestinationDir, "fabric-installer.jar");
}
@Override
public void run() {
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.fabric_dl_progress);
try {
if(runCatching()) mModloaderDownloadListener.onDownloadFinished(mDestinationFile);
}catch (IOException e) {
mModloaderDownloadListener.onDownloadError(e);
}
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, -1, -1);
}
private boolean runCatching() throws IOException {
if(!mDestinationDir.exists() && !mDestinationDir.mkdirs()) throw new IOException("Failed to create cache directory");
String[] urlAndVersion = FabricUtils.getInstallerUrlAndVersion();
if(urlAndVersion == null) {
mModloaderDownloadListener.onDataNotAvailable();
return false;
}
File versionFile = new File(mDestinationDir, "fabric-installer-version");
boolean shouldDownloadInstaller = true;
if(urlAndVersion[1] != null && versionFile.canRead()) { // if we know the latest version that we have and the server has
try {
shouldDownloadInstaller = !urlAndVersion[1].equals(Tools.read(versionFile.getAbsolutePath()));
}catch (IOException e) {
e.printStackTrace();
}
}
if(shouldDownloadInstaller) {
if (urlAndVersion[0] != null) {
byte[] buffer = new byte[8192];
DownloadUtils.downloadFileMonitored(urlAndVersion[0], mDestinationFile, buffer, this);
if(urlAndVersion[1] != null) {
try {
Tools.write(versionFile.getAbsolutePath(), urlAndVersion[1]);
}catch (IOException e) {
e.printStackTrace();
}
}
return true;
} else {
mModloaderDownloadListener.onDataNotAvailable();
return false;
}
}else{
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.fabric_dl_progress);
}
}

View File

@@ -1,83 +0,0 @@
package net.kdt.pojavlaunch.modloaders;
import android.content.Intent;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.utils.DownloadUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class FabricUtils {
private static final String FABRIC_INSTALLER_METADATA_URL = "https://meta.fabricmc.net/v2/versions/installer";
private static final String FABRIC_LOADER_METADATA_URL = "https://meta.fabricmc.net/v2/versions/loader";
public static List<String> downloadLoaderVersionList(boolean onlyStable) throws IOException {
try {
return DownloadUtils.downloadStringCached(FABRIC_LOADER_METADATA_URL,
"fabric_loader_versions", (input)->{
final List<String> loaderList = new ArrayList<>();
try {
enumerateMetadata(input, (object) -> {
if (onlyStable && !object.getBoolean("stable")) return false;
loaderList.add(object.getString("version"));
return false;
});
}catch (JSONException e) {
throw new DownloadUtils.ParseException(e);
}
return loaderList;
});
}catch (DownloadUtils.ParseException e) {
e.printStackTrace();
return null;
}
}
public static String[] getInstallerUrlAndVersion() throws IOException{
String installerMetadata = DownloadUtils.downloadString(FABRIC_INSTALLER_METADATA_URL);
try {
return DownloadUtils.downloadStringCached(FABRIC_INSTALLER_METADATA_URL,
"fabric_installer_versions", input -> {
try {
JSONObject selectedMetadata = enumerateMetadata(installerMetadata, (object) ->
object.getBoolean("stable"));
if (selectedMetadata == null) return null;
return new String[]{selectedMetadata.getString("url"),
selectedMetadata.getString("version")};
} catch (JSONException e) {
throw new DownloadUtils.ParseException(e);
}
});
}catch (DownloadUtils.ParseException e) {
e.printStackTrace();
return null;
}
}
public static void addAutoInstallArgs(Intent intent, File modInstalllerJar,
String gameVersion, String loaderVersion,
boolean isSnapshot, boolean createProfile) {
intent.putExtra("javaArgs", "-jar " + modInstalllerJar.getAbsolutePath() + " client -dir "+ Tools.DIR_GAME_NEW
+ " -mcversion "+gameVersion +" -loader "+loaderVersion +
(isSnapshot ? " -snapshot" : "") +
(createProfile ? "" : " -noprofile"));
intent.putExtra("openLogOutput", true);
}
private static JSONObject enumerateMetadata(String inputMetadata, FabricMetaReader metaReader) throws JSONException{
JSONArray fullMetadata = new JSONArray(inputMetadata);
JSONObject metadataObject = null;
for(int i = 0; i < fullMetadata.length(); i++) {
metadataObject = fullMetadata.getJSONObject(i);
if(metaReader.processMetadata(metadataObject)) return metadataObject;
}
return metadataObject;
}
}

View File

@@ -0,0 +1,24 @@
package net.kdt.pojavlaunch.modloaders;
import androidx.annotation.NonNull;
public class FabricVersion {
public String version;
public boolean stable;
public static class LoaderDescriptor extends FabricVersion {
public FabricVersion loader;
@NonNull
@Override
public String toString() {
return loader != null ? loader.toString() : "null";
}
}
@NonNull
@Override
public String toString() {
return version;
}
}

View File

@@ -0,0 +1,76 @@
package net.kdt.pojavlaunch.modloaders;
import com.kdt.mcgui.ProgressLayout;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import net.kdt.pojavlaunch.utils.DownloadUtils;
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
public class FabriclikeDownloadTask implements Runnable, Tools.DownloaderFeedback{
private final ModloaderDownloadListener mModloaderDownloadListener;
private final FabriclikeUtils mUtils;
private final String mGameVersion;
private final String mLoaderVersion;
private final boolean mCreateProfile;
public FabriclikeDownloadTask(ModloaderDownloadListener modloaderDownloadListener, FabriclikeUtils utils, String mGameVersion, String mLoaderVersion, boolean mCreateProfile) {
this.mModloaderDownloadListener = modloaderDownloadListener;
this.mUtils = utils;
this.mGameVersion = mGameVersion;
this.mLoaderVersion = mLoaderVersion;
this.mCreateProfile = mCreateProfile;
}
@Override
public void run() {
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.fabric_dl_progress);
try {
if(runCatching()) mModloaderDownloadListener.onDownloadFinished(null);
else mModloaderDownloadListener.onDataNotAvailable();
}catch (IOException e) {
mModloaderDownloadListener.onDownloadError(e);
}
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
}
private boolean runCatching() throws IOException{
String fabricJson = DownloadUtils.downloadString(mUtils.createJsonDownloadUrl(mGameVersion, mLoaderVersion));
String versionId;
try {
JSONObject fabricJsonObject = new JSONObject(fabricJson);
versionId = fabricJsonObject.getString("id");
}catch (JSONException e) {
e.printStackTrace();
return false;
}
File versionJsonDir = new File(Tools.DIR_HOME_VERSION, versionId);
File versionJsonFile = new File(versionJsonDir, versionId+".json");
if(versionJsonDir.isFile()) throw new IOException("Version destination directory is a file!");
if(!versionJsonDir.exists() && !versionJsonDir.mkdirs()) throw new IOException("Failed to create version directory");
Tools.write(versionJsonFile.getAbsolutePath(), fabricJson);
if(mCreateProfile) {
LauncherProfiles.load();
MinecraftProfile fabricProfile = new MinecraftProfile();
fabricProfile.lastVersionId = versionId;
fabricProfile.name = mUtils.getName();
fabricProfile.icon = mUtils.getIconName();
LauncherProfiles.insertMinecraftProfile(fabricProfile);
LauncherProfiles.write();
}
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.fabric_dl_progress, mUtils.getName());
}
}

View File

@@ -0,0 +1,107 @@
package net.kdt.pojavlaunch.modloaders;
import com.google.gson.JsonSyntaxException;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.utils.DownloadUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class FabriclikeUtils {
public static final FabriclikeUtils FABRIC_UTILS = new FabriclikeUtils("https://meta.fabricmc.net/v2", "fabric", "Fabric", "fabric");
public static final FabriclikeUtils QUILT_UTILS = new FabriclikeUtils("https://meta.quiltmc.org/v3", "quilt", "Quilt", "quilt");
private static final String LOADER_METADATA_URL = "%s/versions/loader/%s";
private static final String GAME_METADATA_URL = "%s/versions/game";
private static final String JSON_DOWNLOAD_URL = "%s/versions/loader/%s/%s/profile/json";
private final String mApiUrl;
private final String mCachePrefix;
private final String mName;
private final String mIconName;
private FabriclikeUtils(String mApiUrl, String cachePrefix, String mName, String iconName) {
this.mApiUrl = mApiUrl;
this.mCachePrefix = cachePrefix;
this.mIconName = iconName;
this.mName = mName;
}
public FabricVersion[] downloadGameVersions() throws IOException{
try {
return DownloadUtils.downloadStringCached(String.format(GAME_METADATA_URL, mApiUrl), mCachePrefix+"_game_versions",
FabriclikeUtils::deserializeRawVersions
);
}catch (DownloadUtils.ParseException ignored) {}
return null;
}
public FabricVersion[] downloadLoaderVersions(String gameVersion) throws IOException{
try {
String urlEncodedGameVersion = URLEncoder.encode(gameVersion, "UTF-8");
return DownloadUtils.downloadStringCached(String.format(LOADER_METADATA_URL, mApiUrl, urlEncodedGameVersion),
mCachePrefix+"_loader_versions."+urlEncodedGameVersion,
(input)->{ try {
return deserializeLoaderVersions(input);
}catch (JSONException e) {
throw new DownloadUtils.ParseException(e);
}});
}catch (DownloadUtils.ParseException e) {
e.printStackTrace();
}
return null;
}
public String createJsonDownloadUrl(String gameVersion, String loaderVersion) {
try {
gameVersion = URLEncoder.encode(gameVersion, "UTF-8");
loaderVersion = URLEncoder.encode(loaderVersion, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return String.format(JSON_DOWNLOAD_URL, mApiUrl, gameVersion, loaderVersion);
}
public String getName() {
return mName;
}
public String getIconName() {
return mIconName;
}
private static FabricVersion[] deserializeLoaderVersions(String input) throws JSONException {
JSONArray jsonArray = new JSONArray(input);
FabricVersion[] fabricVersions = new FabricVersion[jsonArray.length()];
for(int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i).getJSONObject("loader");
FabricVersion fabricVersion = new FabricVersion();
fabricVersion.version = jsonObject.getString("version");
//Quilt has a skill issue and does not say which versions are stable or not
if(jsonObject.has("stable")) {
fabricVersion.stable = jsonObject.getBoolean("stable");
} else {
fabricVersion.stable = !fabricVersion.version.contains("beta");
}
fabricVersions[i] = fabricVersion;
}
return fabricVersions;
}
private static FabricVersion[] deserializeRawVersions(String jsonArrayIn) throws DownloadUtils.ParseException {
try {
return Tools.GLOBAL_GSON.fromJson(jsonArrayIn, FabricVersion[].class);
}catch (JsonSyntaxException e) {
e.printStackTrace();
throw new DownloadUtils.ParseException(null);
}
}
}

View File

@@ -10,39 +10,79 @@ import net.kdt.pojavlaunch.utils.DownloadUtils;
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);
try {
byte[] buffer = new byte[8192];
DownloadUtils.downloadFileMonitored(mForgeUrl, mDestinationFile, buffer, this);
mListener.onDownloadFinished(mDestinationFile);
}catch (IOException e) {
if(e instanceof FileNotFoundException) {
mListener.onDataNotAvailable();
}else{
mListener.onDownloadError(e);
}
if(determineDownloadUrl()) {
downloadForge();
}
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, -1, -1);
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, mForgeVersion);
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(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();
return false;
}
}catch (IOException e) {
mListener.onDownloadError(e);
return false;
}
return true;
}
public boolean findVersion() throws IOException {
List<String> 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;
}
}

View File

@@ -59,4 +59,10 @@ public class ForgeUtils {
" -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);
}
}

View File

@@ -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 class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback,
}catch (IOException e) {
mListener.onDownloadError(e);
}
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, -1, -1);
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
}
public boolean runCatching() throws IOException {

View File

@@ -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<RecyclerView.ViewHolder> 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<String> 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<ViewHolder> 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<String> 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);
}
}

View File

@@ -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", ""));
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, String> 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> T get(String endpoint, Class<T> tClass) {
return getFullUrl(additionalHeaders, baseUrl + "/" + endpoint, tClass);
}
public <T> T get(String endpoint, HashMap<String, Object> query, Class<T> tClass) {
return getFullUrl(additionalHeaders, baseUrl + "/" + endpoint, query, tClass);
}
public <T> T post(String endpoint, T body, Class<T> tClass) {
return postFullUrl(additionalHeaders, baseUrl + "/" + endpoint, body, tClass);
}
public <T> T post(String endpoint, HashMap<String, Object> query, T body, Class<T> 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<String, String> 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<String, String> 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<String, String> headers) {
if(headers != null) {
for(String key : headers.keySet())
connection.addRequestProperty(key, headers.get(key));
}
}
private static String parseQueries(HashMap<String, Object> 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> T getFullUrl(String url, Class<T> tClass) {
return getFullUrl(null, url, tClass);
}
public static <T> T getFullUrl(String url, HashMap<String, Object> query, Class<T> tClass) {
return getFullUrl(null, url, query, tClass);
}
public static <T> T postFullUrl(String url, T body, Class<T> tClass) {
return postFullUrl(null, url, body, tClass);
}
public static <T> T postFullUrl(String url, HashMap<String, Object> query, T body, Class<T> tClass) {
return postFullUrl(null, url, query, body, tClass);
}
public static <T> T getFullUrl(Map<String, String> headers, String url, Class<T> tClass) {
return new Gson().fromJson(getRaw(headers, url), tClass);
}
public static <T> T getFullUrl(Map<String, String> headers, String url, HashMap<String, Object> query, Class<T> tClass) {
return getFullUrl(headers, url + parseQueries(query), tClass);
}
public static <T> T postFullUrl(Map<String, String> headers, String url, T body, Class<T> tClass) {
return new Gson().fromJson(postRaw(headers, url, body.toString()), tClass);
}
public static <T> T postFullUrl(Map<String, String> headers, String url, HashMap<String, Object> query, T body, Class<T> 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");
}
}
}

View File

@@ -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<ModItem[]> 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<ModItem[]> 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<SearchResult> {
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];
}
}

View File

@@ -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<String, Object> 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<ModItem> 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<JsonObject> 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<JsonObject> objectList, int index, String modId) {
HashMap<String, Object> 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;
}
}

View File

@@ -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<byte[]> sThreadLocalBuffer = new ThreadLocal<>();
private final ThreadPoolExecutor mDownloadPool = new ThreadPoolExecutor(4,4,100, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
private final AtomicBoolean mTerminator = new AtomicBoolean(false);
private final AtomicLong mDownloadSize = new AtomicLong(0);
private final Object mExceptionSyncPoint = new Object();
private final File mDestinationDirectory;
private 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;
}
}

View File

@@ -0,0 +1,105 @@
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.FabriclikeDownloadTask;
import net.kdt.pojavlaunch.modloaders.FabriclikeUtils;
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:
return "quilt-loader-"+modLoaderVersion+"-"+minecraftVersion;
default:
return null;
}
}
/**
* Get the Runnable that needs to run in order to download the mod loader.
* The task will also install the mod loader if it does not require GUI installation
* @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 createFabriclikeTask(listener, FabriclikeUtils.FABRIC_UTILS);
case MOD_LOADER_QUILT:
return createFabriclikeTask(listener, FabriclikeUtils.QUILT_UTILS);
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.
* This method returns null if the mod loader does not require GUI installation
* @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_QUILT:
case MOD_LOADER_FABRIC:
default:
return null;
}
}
/**
* Check whether the mod loader this object denotes requires GUI installation
* @return true if mod loader requires GUI installation, false otherwise
*/
public boolean requiresGuiInstallation() {
switch (modLoaderType) {
case MOD_LOADER_FORGE:
return true;
case MOD_LOADER_FABRIC:
case MOD_LOADER_QUILT:
default:
return false;
}
}
private FabriclikeDownloadTask createFabriclikeTask(ModloaderDownloadListener modloaderDownloadListener, FabriclikeUtils utils) {
return new FabriclikeDownloadTask(modloaderDownloadListener, utils, minecraftVersion, modLoaderVersion, false);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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<String, Object> 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<responseHits.size(); ++i){
JsonObject hit = responseHits.get(i).getAsJsonObject();
items[i] = new ModItem(
Constants.SOURCE_MODRINTH,
hit.get("project_type").getAsString().equals("modpack"),
hit.get("project_id").getAsString(),
hit.get("title").getAsString(),
hit.get("description").getAsString(),
hit.get("icon_url").getAsString()
);
}
if(modrinthSearchResult == null) modrinthSearchResult = new ModrinthSearchResult();
modrinthSearchResult.previousOffset += responseHits.size();
modrinthSearchResult.results = items;
modrinthSearchResult.totalResultCount = response.get("total_hits").getAsInt();
return modrinthSearchResult;
}
@Override
public ModDetail getModDetails(ModItem item) {
JsonArray response = mApiHandler.get(String.format("project/%s/version", item.id), JsonArray.class);
if(response == null) return null;
System.out.println(response);
String[] names = new String[response.size()];
String[] mcNames = new String[response.size()];
String[] urls = new String[response.size()];
for (int i=0; i<response.size(); ++i) {
JsonObject version = response.get(i).getAsJsonObject();
names[i] = version.get("name").getAsString();
mcNames[i] = version.get("game_versions").getAsJsonArray().get(0).getAsString();
urls[i] = version.get("files").getAsJsonArray().get(0).getAsJsonObject().get("url").getAsString();
}
return new ModDetail(item, names, mcNames, urls);
}
@Override
public ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException{
//TODO considering only modpacks for now
return ModpackInstaller.installModpack(modDetail, selectedVersion, this::installMrpack);
}
private static ModLoader createInfo(ModrinthIndex modrinthIndex) {
if(modrinthIndex == null) return null;
Map<String, String> dependencies = modrinthIndex.dependencies;
String mcVersion = dependencies.get("minecraft");
if(mcVersion == null) return null;
String modLoaderVersion;
if((modLoaderVersion = dependencies.get("forge")) != null) {
return new 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;
}
}

View File

@@ -0,0 +1,69 @@
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) {
if(mModLoader.requiresGuiInstallation()) {
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());
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<File> 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);
}
}
}
}

View File

@@ -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);
}

View File

@@ -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<WeakReference<ImageReceiver>> 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<WeakReference<ImageReceiver>> iterator = mCancelledReceivers.iterator();
while (iterator.hasNext()) {
WeakReference<ImageReceiver> 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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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";
}

View File

@@ -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;
}
}

View File

@@ -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<versionNames.length; i++){
if (!versionNames[i].contains(mcVersionNames[i]))
versionNames[i] += " - " + mcVersionNames[i];
}
}
@NonNull
@Override
public String toString() {
return "ModDetail{" +
"versionNames=" + Arrays.toString(versionNames) +
", mcVersionNames=" + Arrays.toString(mcVersionNames) +
", versionIds=" + Arrays.toString(versionUrls) +
", id='" + id + '\'' +
", title='" + title + '\'' +
", description='" + description + '\'' +
", imageUrl='" + imageUrl + '\'' +
", apiSource=" + apiSource +
", isModpack=" + isModpack +
'}';
}
}

View File

@@ -0,0 +1,37 @@
package net.kdt.pojavlaunch.modloaders.modpacks.models;
import androidx.annotation.NonNull;
public class ModItem extends ModSource {
public String id;
public String title;
public String description;
public String imageUrl;
public ModItem(int apiSource, boolean isModpack, String id, String title, String description, String imageUrl) {
this.apiSource = apiSource;
this.isModpack = isModpack;
this.id = id;
this.title = title;
this.description = description;
this.imageUrl = imageUrl;
}
@NonNull
@Override
public String toString() {
return "ModItem{" +
"id='" + id + '\'' +
", title='" + title + '\'' +
", description='" + description + '\'' +
", imageUrl='" + imageUrl + '\'' +
", apiSource=" + apiSource +
", isModpack=" + isModpack +
'}';
}
public String getIconCacheTag() {
return apiSource+"_"+id;
}
}

View File

@@ -0,0 +1,6 @@
package net.kdt.pojavlaunch.modloaders.modpacks.models;
public abstract class ModSource {
public int apiSource;
public boolean isModpack;
}

View File

@@ -0,0 +1,89 @@
package net.kdt.pojavlaunch.modloaders.modpacks.models;
import androidx.annotation.NonNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Map;
/**
* POJO to represent the modrinth index inside mrpacks
*/
public class ModrinthIndex {
public int formatVersion;
public String game;
public String versionId;
public String name;
public String summary;
public ModrinthIndexFile[] files;
public Map<String, String> 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) +
'}';
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,6 @@
package net.kdt.pojavlaunch.modloaders.modpacks.models;
public class SearchResult {
public int totalResultCount;
public ModItem[] results;
}

View File

@@ -56,7 +56,7 @@ public class RTSpinnerAdapter implements SpinnerAdapter {
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = convertView != null?
convertView:
LayoutInflater.from(mContext).inflate(android.R.layout.simple_list_item_1, parent,false);
LayoutInflater.from(mContext).inflate(R.layout.item_simple_list_1, parent,false);
Runtime runtime = mRuntimes.get(position);
if(position == mRuntimes.size() - 1 ){

View File

@@ -31,15 +31,10 @@ public class ProfileAdapter extends BaseAdapter {
private Map<String, MinecraftProfile> mProfiles;
private final MinecraftProfile dummy = new MinecraftProfile();
private List<String> 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])));
public ProfileAdapter(ProfileAdapterExtra[] extraEntries) {
reloadProfiles(extraEntries);
}
/*
* Gets how much profiles are loaded in the adapter right now
@@ -67,6 +62,8 @@ public class ProfileAdapter extends BaseAdapter {
return null;
}
public int resolveProfileIndex(String name) {
return mProfileList.indexOf(name);
}
@@ -98,11 +95,7 @@ public class ProfileAdapter extends BaseAdapter {
MinecraftProfile minecraftProfile = mProfiles.get(nm);
if(minecraftProfile == null) minecraftProfile = dummy;
Drawable cachedIcon = ProfileIconCache.getCachedIcon(nm);
if(cachedIcon == null) {
cachedIcon = ProfileIconCache.tryResolveIcon(v.getResources(), nm, minecraftProfile.icon);
}
Drawable cachedIcon = ProfileIconCache.fetchIcon(v.getResources(), nm, minecraftProfile.icon);
extendedTextView.setCompoundDrawablesRelative(cachedIcon, null, extendedTextView.getCompoundsDrawables()[2], null);
if(Tools.isValidString(minecraftProfile.name))
@@ -134,4 +127,19 @@ public class ProfileAdapter extends BaseAdapter {
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();
}
}

View File

@@ -1,57 +1,99 @@
package net.kdt.pojavlaunch.profiles;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.Base64;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;
import net.kdt.pojavlaunch.R;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class ProfileIconCache {
private static final String BASE64_PNG_HEADER = "data:image/png;base64,";
// Data header format: data:<mime>;<encoding>,<data>
private static final String DATA_HEADER = "data:";
private static final String FALLBACK_ICON_NAME = "default";
private static final Map<String, Drawable> sIconCache = new HashMap<>();
private static Drawable sDefaultIcon;
private static final Map<String, Drawable> sStaticIconCache = new HashMap<>();
public static void initDefault(Context context) {
if(sDefaultIcon != null) return;
sDefaultIcon = ResourcesCompat.getDrawable(context.getResources(), R.mipmap.ic_launcher_foreground, null);
if(sDefaultIcon != null) sDefaultIcon.setBounds(0, 0, 10, 10);
/**
* Fetch an icon from the cache, or load it if it's not cached.
* @param resources the Resources object, used for creating drawables
* @param key the profile key
* @param icon the profile icon data (stored in the icon field of MinecraftProfile)
* @return an icon drawable
*/
public static @NonNull Drawable fetchIcon(Resources resources, @NonNull String key, @Nullable String icon) {
Drawable cachedIcon = sIconCache.get(key);
if(cachedIcon != null) return cachedIcon;
if(icon != null && icon.startsWith(DATA_HEADER)) return fetchDataIcon(resources, key, icon);
else return fetchStaticIcon(resources, key, icon);
}
public static Drawable getCachedIcon(String key) {
return sIconCache.get(key);
private static Drawable fetchDataIcon(Resources resources, String key, @NonNull String icon) {
Drawable dataIcon = readDataIcon(resources, icon);
if(dataIcon == null) dataIcon = fetchFallbackIcon(resources);
sIconCache.put(key, dataIcon);
return dataIcon;
}
public static Drawable submitIcon(Resources resources, String key, String base64) {
byte[] pngBytes = Base64.decode(base64, Base64.DEFAULT);
Drawable drawable = new BitmapDrawable(resources, BitmapFactory.decodeByteArray(pngBytes, 0, pngBytes.length));
sIconCache.put(key, drawable);
return drawable;
}
public static Drawable tryResolveIcon(Resources resources, String profileName, String b64Icon) {
Drawable icon;
if (b64Icon != null && b64Icon.startsWith(BASE64_PNG_HEADER)) {
icon = ProfileIconCache.submitIcon(resources, profileName, b64Icon.substring(BASE64_PNG_HEADER.length()));
}else{
Log.i("IconParser","Unsupported icon: "+b64Icon);
icon = ProfileIconCache.pushDefaultIcon(profileName);
private static Drawable fetchStaticIcon(Resources resources, String key, @Nullable String icon) {
Drawable staticIcon = sStaticIconCache.get(icon);
if(staticIcon == null) {
if(icon != null) staticIcon = getStaticIcon(resources, icon);
if(staticIcon == null) staticIcon = fetchFallbackIcon(resources);
sStaticIconCache.put(icon, staticIcon);
}
return icon;
sIconCache.put(key, staticIcon);
return staticIcon;
}
public static Drawable pushDefaultIcon(String key) {
sIconCache.put(key, sDefaultIcon);
private static @NonNull Drawable fetchFallbackIcon(Resources resources) {
Drawable fallbackIcon = sStaticIconCache.get(FALLBACK_ICON_NAME);
if(fallbackIcon == null) {
fallbackIcon = Objects.requireNonNull(getStaticIcon(resources, FALLBACK_ICON_NAME));
sStaticIconCache.put(FALLBACK_ICON_NAME, fallbackIcon);
}
return fallbackIcon;
}
return sDefaultIcon;
private static Drawable getStaticIcon(Resources resources, @NonNull String icon) {
int staticIconResource = getStaticIconResource(icon);
if(staticIconResource == -1) return null;
return ResourcesCompat.getDrawable(resources, staticIconResource, null);
}
private static int getStaticIconResource(String icon) {
switch (icon) {
case "default": return R.drawable.ic_pojav_full;
case "fabric": return R.drawable.ic_fabric;
case "quilt": return R.drawable.ic_quilt;
default: return -1;
}
}
private static Drawable readDataIcon(Resources resources, String icon) {
byte[] iconData = extractIconData(icon);
if(iconData == null) return null;
Bitmap iconBitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.length);
if(iconBitmap == null) return null;
return new BitmapDrawable(resources, iconBitmap);
}
private static byte[] extractIconData(String inputString) {
int firstSemicolon = inputString.indexOf(';');
int commaAfterSemicolon = inputString.indexOf(',');
if(firstSemicolon == -1 || commaAfterSemicolon == -1) return null;
String dataEncoding = inputString.substring(firstSemicolon+1, commaAfterSemicolon);
if(!dataEncoding.equals("base64")) return null;
return Base64.decode(inputString.substring(commaAfterSemicolon+1), 0);
}
}

View File

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

View File

@@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat;
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 class GameService extends Service {
}
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
}

View File

@@ -19,6 +19,7 @@ import net.kdt.pojavlaunch.R;
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 class ProgressService extends Service implements TaskCountListener {
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 class ProgressService extends Service implements TaskCountListener {
}
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);

View File

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

View File

@@ -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);
}
}

View File

@@ -285,6 +285,9 @@ public class JREUtils {
purgeArg(userArgs,"-d32");
purgeArg(userArgs,"-d64");
purgeArg(userArgs, "-Xint");
purgeArg(userArgs, "-XX:+UseTransparentHugePages");
purgeArg(userArgs, "-XX:+UseLargePagesInMetaspace");
purgeArg(userArgs, "-XX:+UseLargePages");
purgeArg(userArgs, "-Dorg.lwjgl.opengl.libname");
//Add automatically generated args
@@ -332,6 +335,7 @@ public class JREUtils {
ArrayList<String> overridableArguments = new ArrayList<>(Arrays.asList(
"-Djava.home=" + runtimeHome,
"-Djava.io.tmpdir=" + Tools.DIR_CACHE.getAbsolutePath(),
"-Djna.boot.library.path=" + NATIVE_LIB_DIR,
"-Duser.home=" + Tools.DIR_GAME_HOME,
"-Duser.language=" + System.getProperty("user.language"),
"-Dos.name=Linux",
@@ -340,11 +344,11 @@ public class JREUtils {
"-Dpojav.path.private.account=" + Tools.DIR_ACCOUNT_NEW,
"-Duser.timezone=" + TimeZone.getDefault().getID(),
"-Dorg.lwjgl.vulkan.libname=libvulkan.so",
//LWJGL 3 DEBUG FLAGS
//"-Dorg.lwjgl.util.Debug=true",
//"-Dorg.lwjgl.util.DebugFunctions=true",
//"-Dorg.lwjgl.util.DebugLoader=true",
"-Dorg.lwjgl.util.NoChecks=true",
// GLFW Stub width height
"-Dglfwstub.windowWidth=" + Tools.getDisplayFriendlyRes(currentDisplayMetrics.widthPixels, LauncherPreferences.PREF_SCALE_FACTOR/100F),
"-Dglfwstub.windowHeight=" + Tools.getDisplayFriendlyRes(currentDisplayMetrics.heightPixels, LauncherPreferences.PREF_SCALE_FACTOR/100F),

View File

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

View File

@@ -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;
}

View File

@@ -8,45 +8,77 @@ import net.kdt.pojavlaunch.Tools;
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.Map;
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<>();
}
} else {
Tools.write(launcherProfilesFile.getAbsolutePath(), mainProfileJson.toJson());
}
private static final File launcherProfilesFile = new File(Tools.DIR_GAME_NEW, "launcher_profiles.json");
// insertMissing();
return mainProfileJson;
} catch (Throwable th) {
throw new RuntimeException(th);
/** 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);
}
}
// 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 :(");
return profile;
}
/**
* Insert a new profile into the profile map
* @param minecraftProfile the profile to insert
*/
public static void insertMinecraftProfile(MinecraftProfile minecraftProfile) {
mainProfileJson.profiles.put(getFreeProfileKey(), minecraftProfile);
}
/**
* Pick an unused normalized key to store a new profile with
* @return an unused key
*/
public static String getFreeProfileKey() {
Map<String, MinecraftProfile> profileMap = mainProfileJson.profiles;
String freeKey = UUID.randomUUID().toString();
while(profileMap.get(freeKey) != null) freeKey = UUID.randomUUID().toString();
return freeKey;
}
/**
* For all keys to be UUIDs, effectively isolating profile created by installers
* This avoids certain profiles to be erased by the installer
@@ -67,13 +99,9 @@ public class LauncherProfiles {
}
// Swap the new keys
for(String profileKey: keys){
String uuid = UUID.randomUUID().toString();
while(launcherProfiles.profiles.containsKey(uuid)) {
uuid = UUID.randomUUID().toString();
}
launcherProfiles.profiles.put(uuid, launcherProfiles.profiles.get(profileKey));
for(String profileKey : keys){
MinecraftProfile currentProfile = launcherProfiles.profiles.get(profileKey);
insertMinecraftProfile(currentProfile);
launcherProfiles.profiles.remove(profileKey);
hasNormalized = true;
}

View File

@@ -28,6 +28,13 @@ public class MinecraftProfile {
return TEMPLATE;
}
public static MinecraftProfile getDefaultProfile(){
MinecraftProfile defaultProfile = new MinecraftProfile();
defaultProfile.name = "Default";
defaultProfile.lastVersionId = "1.7.10";
return defaultProfile;
}
public MinecraftProfile(){}
public MinecraftProfile(MinecraftProfile profile){

View File

@@ -27,6 +27,12 @@
#include "utils.h"
#include "ctxbridges/gl_bridge.h"
#define GLFW_CLIENT_API 0x22001
/* Consider GLFW_NO_API as Vulkan API */
#define GLFW_NO_API 0
#define GLFW_OPENGL_API 0x30001
struct PotatoBridge {
/* EGLContext */ void* eglContextOld;
@@ -46,15 +52,10 @@ struct PotatoBridge potatoBridge;
#define RENDERER_GL4ES 1
#define RENDERER_VK_ZINK 2
#define RENDERER_VULKAN 4
void* gbuffer;
void pojav_openGLOnLoad() {
}
void pojav_openGLOnUnload() {
}
void pojavTerminate() {
printf("EGLBridge: Terminating\n");
@@ -225,7 +226,10 @@ int pojavInit() {
pojav_environ->savedWidth = ANativeWindow_getWidth(pojav_environ->pojavWindow);
pojav_environ->savedHeight = ANativeWindow_getHeight(pojav_environ->pojavWindow);
ANativeWindow_setBuffersGeometry(pojav_environ->pojavWindow,pojav_environ->savedWidth,pojav_environ->savedHeight,AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM);
return 1;
}
int pojavInitOpenGL() {
// Only affects GL4ES as of now
const char *forceVsync = getenv("FORCE_VSYNC");
if (strcmp(forceVsync, "true") == 0)
@@ -269,6 +273,25 @@ int pojavInit() {
return 0;
}
void pojavSetWindowHint(int hint, int value) {
if (hint != GLFW_CLIENT_API) return;
switch (value) {
case GLFW_NO_API:
pojav_environ->config_renderer = RENDERER_VULKAN;
/* Nothing to do: initialization is handled in Java-side */
// pojavInitVulkan();
break;
case GLFW_OPENGL_API:
/* Nothing to do: initialization is called in pojavCreateContext */
// pojavInitOpenGL();
break;
default:
printf("GLFW: Unimplemented API 0x%x\n", value);
abort();
}
}
ANativeWindow_Buffer buf;
int32_t stride;
bool stopSwapBuffers;
@@ -344,6 +367,12 @@ Java_org_lwjgl_glfw_GLFW_nativeEglDetachOnCurrentThread(JNIEnv *env, jclass claz
*/
void* pojavCreateContext(void* contextSrc) {
if (pojav_environ->config_renderer == RENDERER_VULKAN) {
return (void *)pojav_environ->pojavWindow;
}
pojavInitOpenGL();
if (pojav_environ->config_renderer == RENDERER_GL4ES) {
/*const EGLint ctx_attribs[] = {
EGL_CONTEXT_CLIENT_VERSION, atoi(getenv("LIBGL_ES")),

View File

@@ -17,6 +17,7 @@
#include <stdlib.h>
#include <string.h>
#include <stdatomic.h>
#include <math.h>
#include "log.h"
#include "utils.h"
@@ -139,7 +140,8 @@ void pojavPumpEvents(void* window) {
if((pojav_environ->cLastX != pojav_environ->cursorX || pojav_environ->cLastY != pojav_environ->cursorY) && pojav_environ->GLFW_invoke_CursorPos) {
pojav_environ->cLastX = pojav_environ->cursorX;
pojav_environ->cLastY = pojav_environ->cursorY;
pojav_environ->GLFW_invoke_CursorPos(window, pojav_environ->cursorX, pojav_environ->cursorY);
pojav_environ->GLFW_invoke_CursorPos(window, floor(pojav_environ->cursorX),
floor(pojav_environ->cursorY));
}
// The out target index is updated by the rewinder

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More