diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 26e63f4b4..385f87a24 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -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 diff --git a/.gitignore b/.gitignore index 07b3f02a6..798bb7677 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ app_pojavlauncher/src/main/assets/components/jre local.properties .idea/ app_pojavlauncher/.cxx/ -.vs/ \ No newline at end of file +.vs/ +/curseforge_key.txt diff --git a/app_pojavlauncher/build.gradle b/app_pojavlauncher/build.gradle index ab5b93bb8..6191e7efe 100644 --- a/app_pojavlauncher/build.gradle +++ b/app_pojavlauncher/build.gradle @@ -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' diff --git a/app_pojavlauncher/src/main/AndroidManifest.xml b/app_pojavlauncher/src/main/AndroidManifest.xml index 457e0cbfd..4147e1f3a 100644 --- a/app_pojavlauncher/src/main/AndroidManifest.xml +++ b/app_pojavlauncher/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ android:name="android.hardware.type.pc" android:required="false" /> + @@ -77,11 +78,15 @@ android:name=".FatalErrorActivity" android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation" android:theme="@style/Theme.AppCompat.DayNight.Dialog" /> + diff --git a/app_pojavlauncher/src/main/assets/components/forge_installer/forge_installer.jar b/app_pojavlauncher/src/main/assets/components/forge_installer/forge_installer.jar index 2e498a39b..c540bee9c 100644 Binary files a/app_pojavlauncher/src/main/assets/components/forge_installer/forge_installer.jar and b/app_pojavlauncher/src/main/assets/components/forge_installer/forge_installer.jar differ diff --git a/app_pojavlauncher/src/main/assets/components/forge_installer/version b/app_pojavlauncher/src/main/assets/components/forge_installer/version index 825b64780..50f426278 100644 --- a/app_pojavlauncher/src/main/assets/components/forge_installer/version +++ b/app_pojavlauncher/src/main/assets/components/forge_installer/version @@ -1 +1 @@ -1688133008591 \ No newline at end of file +1692525087345 \ No newline at end of file diff --git a/app_pojavlauncher/src/main/assets/components/lwjgl3/lwjgl-glfw-classes.jar b/app_pojavlauncher/src/main/assets/components/lwjgl3/lwjgl-glfw-classes.jar index 4b12ae092..de7650cc0 100644 Binary files a/app_pojavlauncher/src/main/assets/components/lwjgl3/lwjgl-glfw-classes.jar and b/app_pojavlauncher/src/main/assets/components/lwjgl3/lwjgl-glfw-classes.jar differ diff --git a/app_pojavlauncher/src/main/assets/components/lwjgl3/version b/app_pojavlauncher/src/main/assets/components/lwjgl3/version index 3a511f8cb..f4d001ea1 100644 --- a/app_pojavlauncher/src/main/assets/components/lwjgl3/version +++ b/app_pojavlauncher/src/main/assets/components/lwjgl3/version @@ -1 +1 @@ -1689180036097 \ No newline at end of file +1687078018167 \ No newline at end of file diff --git a/app_pojavlauncher/src/main/assets/launcher_profiles.json b/app_pojavlauncher/src/main/assets/launcher_profiles.json index 82c2d1761..d0ca29b25 100644 --- a/app_pojavlauncher/src/main/assets/launcher_profiles.json +++ b/app_pojavlauncher/src/main/assets/launcher_profiles.json @@ -2,7 +2,7 @@ "profiles": { "(Default)": { "name": "(Default)", - "lastVersionId": "Unknown" + "lastVersionId": "1.7.10" } }, "selectedProfile": "(Default)" diff --git a/app_pojavlauncher/src/main/java/com/kdt/SimpleArrayAdapter.java b/app_pojavlauncher/src/main/java/com/kdt/SimpleArrayAdapter.java new file mode 100644 index 000000000..4069ae429 --- /dev/null +++ b/app_pojavlauncher/src/main/java/com/kdt/SimpleArrayAdapter.java @@ -0,0 +1,67 @@ +package com.kdt; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.List; + +/** + * Basic adapter, expect it uses the what is passed by the code, no the resources + * @param + */ +public class SimpleArrayAdapter extends BaseAdapter { + private List mObjects; + public SimpleArrayAdapter(List objects) { + setObjects(objects); + } + + public void setObjects(@Nullable List objects) { + if(objects == null){ + if(mObjects != Collections.emptyList()) { + mObjects = Collections.emptyList(); + notifyDataSetChanged(); + } + } else { + if(objects != mObjects){ + mObjects = objects; + notifyDataSetChanged(); + } + } + } + + @Override + public int getCount() { + return mObjects.size(); + } + + @Override + public T getItem(int position) { + return mObjects.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + if(convertView == null){ + convertView = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false); + } + + TextView v = (TextView) convertView; + v.setText(mObjects.get(position).toString()); + return v; + } +} diff --git a/app_pojavlauncher/src/main/java/com/kdt/mcgui/TextProgressBar.java b/app_pojavlauncher/src/main/java/com/kdt/mcgui/TextProgressBar.java index 5273c6c3b..db1862ebc 100644 --- a/app_pojavlauncher/src/main/java/com/kdt/mcgui/TextProgressBar.java +++ b/app_pojavlauncher/src/main/java/com/kdt/mcgui/TextProgressBar.java @@ -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); diff --git a/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcVersionSpinner.java b/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcVersionSpinner.java index d0cd464c2..64ab6c35b 100644 --- a/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcVersionSpinner.java +++ b/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcVersionSpinner.java @@ -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 diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/CustomControlsActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/CustomControlsActivity.java index b8f3e5a4e..c913162ef 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/CustomControlsActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/CustomControlsActivity.java @@ -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)); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JMinecraftVersionList.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JMinecraftVersionList.java index 5b753efa5..c865e3a0b 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JMinecraftVersionList.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JMinecraftVersionList.java @@ -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 { diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JRE17Util.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JRE17Util.java index f004f1307..1cd0dca35 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JRE17Util.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JRE17Util.java @@ -55,7 +55,7 @@ public 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); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java index 76a05254d..cbe045315 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java @@ -24,6 +24,7 @@ import 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; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java index 3b6d8abff..dfad0b9f1 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java @@ -322,24 +322,24 @@ 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) {} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java index acd65d4c7..054bb8d9f 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java @@ -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); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java index ee67bca05..09a22a2ba 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java @@ -18,6 +18,7 @@ import java.util.concurrent.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)); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java new file mode 100644 index 000000000..ec64ee7fb --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java @@ -0,0 +1,75 @@ +package net.kdt.pojavlaunch; + +import android.app.Activity; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; + +import net.kdt.pojavlaunch.contextexecutor.ContextExecutorTask; +import net.kdt.pojavlaunch.value.NotificationConstants; + +import java.io.Serializable; + +public class ShowErrorActivity extends Activity { + + private static final String ERROR_ACTIVITY_REMOTE_TASK = "remoteTask"; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + if(intent == null) { + finish(); + return; + } + RemoteErrorTask remoteErrorTask = (RemoteErrorTask) intent.getSerializableExtra(ERROR_ACTIVITY_REMOTE_TASK); + if(remoteErrorTask == null) { + finish(); + return; + } + remoteErrorTask.executeWithActivity(this); + } + + + public static class RemoteErrorTask implements ContextExecutorTask, Serializable { + private final Throwable mThrowable; + private final String mRolledMsg; + + public RemoteErrorTask(Throwable mThrowable, String mRolledMsg) { + this.mThrowable = mThrowable; + this.mRolledMsg = mRolledMsg; + } + @Override + public void executeWithActivity(Activity activity) { + Tools.showError(activity, mRolledMsg, mThrowable); + } + + @Override + public void executeWithApplication(Context context) { + sendNotification(context, this); + } + } + private static void sendNotification(Context context, RemoteErrorTask remoteErrorTask) { + + Intent showErrorIntent = new Intent(context, ShowErrorActivity.class); + showErrorIntent.putExtra(ERROR_ACTIVITY_REMOTE_TASK, remoteErrorTask); + + PendingIntent pendingIntent = PendingIntent.getActivity(context, NotificationConstants.PENDINGINTENT_CODE_SHOW_ERROR, showErrorIntent, + Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0); + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, context.getString(R.string.notif_channel_id)) + .setContentTitle(context.getString(R.string.notif_error_occured)) + .setContentText(context.getString(R.string.notif_error_occured_desc)) + .setSmallIcon(R.drawable.notif_icon) + .setContentIntent(pendingIntent); + notificationManager.notify(NotificationConstants.NOTIFICATION_ID_SHOW_ERROR, notificationBuilder.build()); + } + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java index 6b32ff9a9..e2f7d711b 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -33,6 +33,7 @@ import android.view.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 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(); + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java new file mode 100644 index 000000000..7b17f5cc0 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java @@ -0,0 +1,75 @@ +package net.kdt.pojavlaunch.contextexecutor; + +import android.app.Activity; +import android.app.Application; + +import net.kdt.pojavlaunch.Tools; + +import java.lang.ref.WeakReference; + +public class ContextExecutor { + private static WeakReference sApplication; + private static WeakReference sActivity; + + + /** + * Schedules a ContextExecutorTask to be executed. For more info on tasks, please read + * ContextExecutorTask.java + * @param contextExecutorTask the task to be executed + */ + public static void execute(ContextExecutorTask contextExecutorTask) { + Tools.runOnUiThread(()->executeOnUiThread(contextExecutorTask)); + } + + private static void executeOnUiThread(ContextExecutorTask contextExecutorTask) { + Activity activity = getWeakReference(sActivity); + if(activity != null) { + contextExecutorTask.executeWithActivity(activity); + return; + } + Application application = getWeakReference(sApplication); + if(application != null) { + contextExecutorTask.executeWithApplication(application); + }else { + throw new RuntimeException("ContextExecutor.execute() called before Application.onCreate!"); + } + } + + /** + * Set the Activity that this ContextExecutor will use for executing tasks + * @param activity the activity to be used + */ + public static void setActivity(Activity activity) { + sActivity = new WeakReference<>(activity); + } + + /** + * Clear the Activity previously set, so thet ContextExecutor won't use it to execute tasks. + */ + public static void clearActivity() { + if(sActivity != null) + sActivity.clear(); + } + + /** + * Set the Application that will be used to execute tasks if the Activity won't be available. + * @param application the application to use as the fallback + */ + public static void setApplication(Application application) { + sApplication = new WeakReference<>(application); + } + + /** + * Clear the Application previously set, so that ContextExecutor will notify the user of a critical error + * that is executing code after the application is ended by the system. + */ + public static void clearApplication() { + if(sApplication != null) + sApplication.clear(); + } + + private static T getWeakReference(WeakReference weakReference) { + if(weakReference == null) return null; + return weakReference.get(); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java new file mode 100644 index 000000000..9d8b1d3c3 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java @@ -0,0 +1,25 @@ +package net.kdt.pojavlaunch.contextexecutor; + +import android.app.Activity; +import android.content.Context; + +/** + * A ContextExecutorTask is a task that can dynamically change its behaviour, based on the context + * used for its execution. This can be used to implement for ex. error/finish notifications from + * background threads that may live with the Service after the activity that started them died. + */ +public interface ContextExecutorTask { + /** + * ContextExecutor will execute this function first if a foreground Activity that was attached to the + * ContextExecutor is available. + * @param activity the activity + */ + void executeWithActivity(Activity activity); + + /** + * ContextExecutor will execute this function if a foreground Activity is not available, but the app + * is still running. + * @param context the application context + */ + void executeWithApplication(Context context); +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/ControlData.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/ControlData.java index 4d50ba983..5f39ec0c2 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/ControlData.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/ControlData.java @@ -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 SPECIAL_BUTTON_NAME_ARRAY; - - // Internal usage only - public boolean isHideable; - private static WeakReference builder = new WeakReference<>(null); - private static WeakReference> conversionMap = new WeakReference<>(null); + private static WeakReference> 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 buildSpecialButtonArray() { - if (SPECIAL_BUTTON_NAME_ARRAY == null) { - List 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 buildSpecialButtonArray() { + if (SPECIAL_BUTTON_NAME_ARRAY == null) { + List 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 fillConversionMap(){ + private Map fillConversionMap() { ArrayMap 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; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/ControlJoystickData.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/ControlJoystickData.java new file mode 100644 index 000000000..994489594 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/ControlJoystickData.java @@ -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); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/ControlLayout.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/ControlLayout.java index 9c5000894..6cb3e2e67 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/ControlLayout.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/ControlLayout.java @@ -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; + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/CustomControls.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/CustomControls.java index 0dc96a006..ffff12d1c 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/CustomControls.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/CustomControls.java @@ -13,16 +13,18 @@ public class CustomControls { public float scaledAt; public List mControlDataList; public List mDrawerDataList; + public List mJoystickDataList; public CustomControls() { - this(new ArrayList<>(), new ArrayList<>()); + this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); } - public CustomControls(List mControlDataList, List mDrawerDataList) { + public CustomControls(List mControlDataList, List mDrawerDataList, List 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)); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/LayoutConverter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/LayoutConverter.java index d8e20a907..89786b011 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/LayoutConverter.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/LayoutConverter.java @@ -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)); + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlButton.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlButton.java index abfd6cb7d..888c85e5d 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlButton.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlButton.java @@ -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); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlDrawer.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlDrawer.java index ccacc8323..cc022ea47 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlDrawer.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlDrawer.java @@ -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") diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlInterface.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlInterface.java index 3f3e6fae9..7f508822e 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlInterface.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlInterface.java @@ -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 - * + *

* 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. - * + *

* 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); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlJoystick.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlJoystick.java new file mode 100644 index 000000000..34c21c424 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlJoystick.java @@ -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; + } + } + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlSubButton.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlSubButton.java index cd24b8dc9..fdb5cfc03 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlSubButton.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlSubButton.java @@ -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); + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlPopup.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlPopup.java index 738e915c8..3d799ecbc 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlPopup.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlPopup.java @@ -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 mAdapter; protected List 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 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); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabricInstallFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabricInstallFragment.java index 4626a1df9..6bcc93b84 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabricInstallFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabricInstallFragment.java @@ -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 mLoaderVersions = FabricUtils.downloadLoaderVersionList(false); - if (mLoaderVersions != null) { - Tools.runOnUiThread(()->{ - Context context = getContext(); - if(context == null) return; - ArrayAdapter 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; } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabriclikeInstallFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabriclikeInstallFragment.java new file mode 100644 index 000000000..8c7d93724 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabriclikeInstallFragment.java @@ -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 createAdapter(FabricVersion[] fabricVersions, boolean onlyStable) { + ArrayList 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); +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ForgeInstallFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ForgeInstallFragment.java index fe609697f..9455a23b4 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ForgeInstallFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ForgeInstallFragment.java @@ -59,7 +59,7 @@ public class ForgeInstallFragment extends ModVersionListFragment> { @Override public Runnable createDownloadTask(Object selectedVersion, ModloaderListenerProxy listenerProxy) { - return new ForgeDownloadTask(listenerProxy, (String) selectedVersion, new File(Tools.DIR_CACHE, "forge-installer.jar")); + return new ForgeDownloadTask(listenerProxy, (String) selectedVersion); } @Override diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java index 29e5b4d33..701a7a76b 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java @@ -13,16 +13,21 @@ import androidx.annotation.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); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModVersionListFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModVersionListFragment.java index f1ac32a51..581e487f0 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModVersionListFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModVersionListFragment.java @@ -58,10 +58,10 @@ public abstract class ModVersionListFragment extends Fragment implements Runn } @Override - public void onDestroyView() { + public void onStop() { ModloaderListenerProxy taskProxy = getTaskProxy(); if(taskProxy != null) taskProxy.detachListener(); - super.onDestroyView(); + super.onStop(); } @Override diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/OptiFineInstallFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/OptiFineInstallFragment.java index a37704618..abe7c0b39 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/OptiFineInstallFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/OptiFineInstallFragment.java @@ -51,8 +51,7 @@ public class OptiFineInstallFragment extends ModVersionListFragment 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); } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileTypeSelectFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileTypeSelectFragment.java index 2c29ffff5..e8d4f86c1 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileTypeSelectFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileTypeSelectFragment.java @@ -31,5 +31,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)); } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/QuiltInstallFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/QuiltInstallFragment.java new file mode 100644 index 000000000..f7356ed3d --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/QuiltInstallFragment.java @@ -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; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java new file mode 100644 index 000000000..ace5d14af --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java @@ -0,0 +1,165 @@ +package net.kdt.pojavlaunch.fragments; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.math.MathUtils; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.modloaders.modpacks.ModItemAdapter; +import net.kdt.pojavlaunch.modloaders.modpacks.api.CommonApi; +import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; +import net.kdt.pojavlaunch.profiles.VersionSelectorDialog; +import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; + +public class SearchModFragment extends Fragment implements ModItemAdapter.SearchResultCallback { + + public static final String TAG = "SearchModFragment"; + private View mOverlay; + private float mOverlayTopCache; // Padding cache reduce resource lookup + + private final RecyclerView.OnScrollListener mOverlayPositionListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + mOverlay.setY(MathUtils.clamp(mOverlay.getY() - dy, -mOverlay.getHeight(), mOverlayTopCache)); + } + }; + + private EditText mSearchEditText; + private ImageButton mFilterButton; + private RecyclerView mRecyclerview; + private ModItemAdapter mModItemAdapter; + private ProgressBar mSearchProgressBar; + private TextView mStatusTextView; + private ColorStateList mDefaultTextColor; + + private ModpackApi modpackApi; + + private final SearchFilters mSearchFilters; + + public SearchModFragment(){ + super(R.layout.fragment_mod_search); + mSearchFilters = new SearchFilters(); + mSearchFilters.isModpack = true; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + modpackApi = new CommonApi(context.getString(R.string.curseforge_api_key)); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + // You can only access resources after attaching to current context + mModItemAdapter = new ModItemAdapter(getResources(), modpackApi, this); + ProgressKeeper.addTaskCountListener(mModItemAdapter); + mOverlayTopCache = getResources().getDimension(R.dimen.fragment_padding_medium); + + mOverlay = view.findViewById(R.id.search_mod_overlay); + mSearchEditText = view.findViewById(R.id.search_mod_edittext); + mSearchProgressBar = view.findViewById(R.id.search_mod_progressbar); + mRecyclerview = view.findViewById(R.id.search_mod_list); + mStatusTextView = view.findViewById(R.id.search_mod_status_text); + mFilterButton = view.findViewById(R.id.search_mod_filter); + + mDefaultTextColor = mStatusTextView.getTextColors(); + + mRecyclerview.setLayoutManager(new LinearLayoutManager(getContext())); + mRecyclerview.setAdapter(mModItemAdapter); + + mRecyclerview.addOnScrollListener(mOverlayPositionListener); + + mSearchEditText.setOnEditorActionListener((v, actionId, event) -> { + mSearchProgressBar.setVisibility(View.VISIBLE); + mSearchFilters.name = mSearchEditText.getText().toString(); + mModItemAdapter.performSearchQuery(mSearchFilters); + return true; + }); + + mOverlay.post(()->{ + int overlayHeight = mOverlay.getHeight(); + mRecyclerview.setPadding(mRecyclerview.getPaddingLeft(), + mRecyclerview.getPaddingTop() + overlayHeight, + mRecyclerview.getPaddingRight(), + mRecyclerview.getPaddingBottom()); + }); + mFilterButton.setOnClickListener(v -> displayFilterDialog()); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + ProgressKeeper.removeTaskCountListener(mModItemAdapter); + mRecyclerview.removeOnScrollListener(mOverlayPositionListener); + } + + @Override + public void onSearchFinished() { + mSearchProgressBar.setVisibility(View.GONE); + mStatusTextView.setVisibility(View.GONE); + } + + @Override + public void onSearchError(int error) { + mSearchProgressBar.setVisibility(View.GONE); + mStatusTextView.setVisibility(View.VISIBLE); + switch (error) { + case ERROR_INTERNAL: + mStatusTextView.setTextColor(Color.RED); + mStatusTextView.setText(R.string.search_modpack_error); + break; + case ERROR_NO_RESULTS: + mStatusTextView.setTextColor(mDefaultTextColor); + mStatusTextView.setText(R.string.search_modpack_no_result); + break; + } + } + + private void displayFilterDialog() { + AlertDialog dialog = new AlertDialog.Builder(requireContext()) + .setView(R.layout.dialog_mod_filters) + .create(); + + // setup the view behavior + dialog.setOnShowListener(dialogInterface -> { + TextView mSelectedVersion = dialog.findViewById(R.id.search_mod_selected_mc_version_textview); + Button mSelectVersionButton = dialog.findViewById(R.id.search_mod_mc_version_button); + Button mApplyButton = dialog.findViewById(R.id.search_mod_apply_filters); + + assert mSelectVersionButton != null; + assert mSelectedVersion != null; + assert mApplyButton != null; + + // Setup the expendable list behavior + mSelectVersionButton.setOnClickListener(v -> VersionSelectorDialog.open(v.getContext(), true, (id, snapshot)-> mSelectedVersion.setText(id))); + + // Apply visually all the current settings + mSelectedVersion.setText(mSearchFilters.mcVersion); + + // Apply the new settings + mApplyButton.setOnClickListener(v -> { + mSearchFilters.mcVersion = mSelectedVersion.getText().toString(); + dialogInterface.dismiss(); + }); + }); + + + dialog.show(); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricDownloadTask.java deleted file mode 100644 index 03ea723ad..000000000 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricDownloadTask.java +++ /dev/null @@ -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); - } -} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricUtils.java deleted file mode 100644 index 83f849a78..000000000 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricUtils.java +++ /dev/null @@ -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 downloadLoaderVersionList(boolean onlyStable) throws IOException { - try { - return DownloadUtils.downloadStringCached(FABRIC_LOADER_METADATA_URL, - "fabric_loader_versions", (input)->{ - final List 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; - } -} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricVersion.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricVersion.java new file mode 100644 index 000000000..b1a5358b7 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricVersion.java @@ -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; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabriclikeDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabriclikeDownloadTask.java new file mode 100644 index 000000000..706acbce3 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabriclikeDownloadTask.java @@ -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()); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabriclikeUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabriclikeUtils.java new file mode 100644 index 000000000..9dddb0049 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabriclikeUtils.java @@ -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); + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java index 60dcd9b2d..81d7f1d1a 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java @@ -10,39 +10,79 @@ import 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 forgeVersions = ForgeUtils.downloadForgeVersions(); + if(forgeVersions == null) return false; + String versionStart = mGameVersion+"-"+mLoaderVersion; + for(String versionName : forgeVersions) { + if(!versionName.startsWith(versionStart)) continue; + mFullVersion = versionName; + mDownloadUrl = ForgeUtils.getInstallerUrl(mFullVersion); + return true; + } + return false; } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeUtils.java index 31420de90..5925d9b4d 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeUtils.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeUtils.java @@ -59,4 +59,10 @@ public 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); + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/OptiFineDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/OptiFineDownloadTask.java index c5278340d..d4fcfab68 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/OptiFineDownloadTask.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/OptiFineDownloadTask.java @@ -22,9 +22,9 @@ public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback, private final Object mMinecraftDownloadLock = new Object(); private Throwable mDownloaderThrowable; - public OptiFineDownloadTask(OptiFineUtils.OptiFineVersion mOptiFineVersion, File mDestinationFile, ModloaderDownloadListener mListener) { + public OptiFineDownloadTask(OptiFineUtils.OptiFineVersion mOptiFineVersion, ModloaderDownloadListener mListener) { this.mOptiFineVersion = mOptiFineVersion; - this.mDestinationFile = mDestinationFile; + this.mDestinationFile = new File(Tools.DIR_CACHE, "optifine-installer.jar"); this.mListener = mListener; } @@ -36,7 +36,7 @@ public 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 { diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModItemAdapter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModItemAdapter.java new file mode 100644 index 000000000..5eab19ab0 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModItemAdapter.java @@ -0,0 +1,409 @@ +package net.kdt.pojavlaunch.modloaders.modpacks; + +import android.annotation.SuppressLint; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.graphics.drawable.RoundedBitmapDrawable; +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; +import androidx.recyclerview.widget.RecyclerView; + +import com.kdt.SimpleArrayAdapter; + +import net.kdt.pojavlaunch.PojavApplication; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi; +import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ImageReceiver; +import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ModIconCache; +import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult; +import net.kdt.pojavlaunch.progresskeeper.TaskCountListener; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.Future; + +public class ModItemAdapter extends RecyclerView.Adapter implements TaskCountListener { + private static final ModItem[] MOD_ITEMS_EMPTY = new ModItem[0]; + private static final int VIEW_TYPE_MOD_ITEM = 0; + private static final int VIEW_TYPE_LOADING = 1; + + /* Used when versions haven't loaded yet, default text to reduce layout shifting */ + private final SimpleArrayAdapter mLoadingAdapter = new SimpleArrayAdapter<>(Collections.singletonList("Loading")); + /* This my seem horribly inefficient but it is in fact the most efficient way without effectively writing a weak collection from scratch */ + private final Set mViewHolderSet = Collections.newSetFromMap(new WeakHashMap<>()); + private final ModIconCache mIconCache = new ModIconCache(); + private final SearchResultCallback mSearchResultCallback; + private ModItem[] mModItems; + private final ModpackApi mModpackApi; + + /* Cache for ever so slightly rounding the image for the corner not to stick out of the layout */ + private final float mCornerDimensionCache; + + private Future mTaskInProgress; + private SearchFilters mSearchFilters; + private SearchResult mCurrentResult; + private boolean mLastPage; + private boolean mTasksRunning; + + + public ModItemAdapter(Resources resources, ModpackApi api, SearchResultCallback callback) { + mCornerDimensionCache = resources.getDimension(R.dimen._1sdp) / 250; + mModpackApi = api; + mModItems = new ModItem[]{}; + mSearchResultCallback = callback; + } + + public void performSearchQuery(SearchFilters searchFilters) { + if(mTaskInProgress != null) { + mTaskInProgress.cancel(true); + mTaskInProgress = null; + } + this.mSearchFilters = searchFilters; + this.mLastPage = false; + mTaskInProgress = new SelfReferencingFuture(new SearchApiTask(mSearchFilters, null)) + .startOnExecutor(PojavApplication.sExecutorService); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + LayoutInflater layoutInflater = LayoutInflater.from(viewGroup.getContext()); + View view; + switch (viewType) { + case VIEW_TYPE_MOD_ITEM: + // Create a new view, which defines the UI of the list item + view = layoutInflater.inflate(R.layout.view_mod, viewGroup, false); + return new ViewHolder(view); + case VIEW_TYPE_LOADING: + // Create a new view, which is actually just the progress bar + view = layoutInflater.inflate(R.layout.view_loading, viewGroup, false); + return new LoadingViewHolder(view); + default: + throw new RuntimeException("Unimplemented view type!"); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + switch (getItemViewType(position)) { + case VIEW_TYPE_MOD_ITEM: + ((ModItemAdapter.ViewHolder)holder).setStateLimited(mModItems[position]); + break; + case VIEW_TYPE_LOADING: + loadMoreResults(); + break; + default: + throw new RuntimeException("Unimplemented view type!"); + } + } + + @Override + public int getItemCount() { + if(mLastPage || mModItems.length == 0) return mModItems.length; + return mModItems.length+1; + } + + private void loadMoreResults() { + if(mTaskInProgress != null) return; + mTaskInProgress = new SelfReferencingFuture(new SearchApiTask(mSearchFilters, mCurrentResult)) + .startOnExecutor(PojavApplication.sExecutorService); + } + + @Override + public int getItemViewType(int position) { + if(position < mModItems.length) return VIEW_TYPE_MOD_ITEM; + return VIEW_TYPE_LOADING; + } + + @Override + public void onUpdateTaskCount(int taskCount) { + Tools.runOnUiThread(()->{ + mTasksRunning = taskCount != 0; + for(ViewHolder viewHolder : mViewHolderSet) { + viewHolder.updateInstallButtonState(); + } + }); + } + + + /** + * Basic viewholder with expension capabilities + */ + public class ViewHolder extends RecyclerView.ViewHolder { + + private ModDetail mModDetail = null; + private ModItem mModItem = null; + private final TextView mTitle, mDescription; + private final ImageView mIconView, mSourceView; + private View mExtendedLayout; + private Spinner mExtendedSpinner; + private Button mExtendedButton; + private TextView mExtendedErrorTextView; + private Future mExtensionFuture; + private Bitmap mThumbnailBitmap; + private ImageReceiver mImageReceiver; + private boolean mInstallEnabled; + + /* Used to display available versions of the mod(pack) */ + private final SimpleArrayAdapter mVersionAdapter = new SimpleArrayAdapter<>(null); + + public ViewHolder(View view) { + super(view); + mViewHolderSet.add(this); + view.setOnClickListener(v -> { + if(!hasExtended()){ + // Inflate the ViewStub + mExtendedLayout = ((ViewStub)v.findViewById(R.id.mod_limited_state_stub)).inflate(); + mExtendedButton = mExtendedLayout.findViewById(R.id.mod_extended_select_version_button); + mExtendedSpinner = mExtendedLayout.findViewById(R.id.mod_extended_version_spinner); + mExtendedErrorTextView = mExtendedLayout.findViewById(R.id.mod_extended_error_textview); + + mExtendedButton.setOnClickListener(v1 -> mModpackApi.handleInstallation( + mExtendedButton.getContext().getApplicationContext(), + mModDetail, + mExtendedSpinner.getSelectedItemPosition())); + mExtendedSpinner.setAdapter(mLoadingAdapter); + } else { + if(isExtended()) closeDetailedView(); + else openDetailedView(); + } + + if(isExtended() && mModDetail == null && mExtensionFuture == null) { // only reload if no reloads are in progress + setDetailedStateDefault(); + /* + * Why do we do this? + * The reason is simple: multithreading is difficult as hell to manage + * Let me explain: + */ + mExtensionFuture = new SelfReferencingFuture(myFuture -> { + /* + * While we are sitting in the function below doing networking, the view might have already gotten recycled. + * If we didn't use a Future, we would have extended a ViewHolder with completely unrelated content + * or with an error that has never actually happened + */ + mModDetail = mModpackApi.getModDetails(mModItem); + System.out.println(mModDetail); + Tools.runOnUiThread(() -> { + /* + * Once we enter here, the state we're in is already defined - no view shuffling can happen on the UI + * thread while we are on the UI thread ourselves. If we were cancelled, this means that the future + * we were supposed to have no longer makes sense, so we return and do not alter the state (since we might + * alter the state of an unrelated item otherwise) + */ + if(myFuture.isCancelled()) return; + /* + * We do not null the future before returning since this field might already belong to a different item with its + * own Future, which we don't want to interfere with. + * But if the future is not cancelled, it is the right one for this ViewHolder, and we don't need it anymore, so + * let's help GC clean it up once we exit! + */ + mExtensionFuture = null; + setStateDetailed(mModDetail); + }); + }).startOnExecutor(PojavApplication.sExecutorService); + } + }); + + // Define click listener for the ViewHolder's View + mTitle = view.findViewById(R.id.mod_title_textview); + mDescription = view.findViewById(R.id.mod_body_textview); + mIconView = view.findViewById(R.id.mod_thumbnail_imageview); + mSourceView = view.findViewById(R.id.mod_source_imageview); + } + + /** Display basic info about the moditem */ + public void setStateLimited(ModItem item) { + mModDetail = null; + if(mThumbnailBitmap != null) { + mIconView.setImageBitmap(null); + mThumbnailBitmap.recycle(); + } + if(mImageReceiver != null) { + mIconCache.cancelImage(mImageReceiver); + } + if(mExtensionFuture != null) { + /* + * Since this method reinitializes the ViewHolder for a new mod, this Future stops being ours, so we cancel it + * and null it. The rest is handled above + */ + mExtensionFuture.cancel(true); + mExtensionFuture = null; + } + + mModItem = item; + // here the previous reference to the image receiver will disappear + mImageReceiver = bm->{ + mImageReceiver = null; + mThumbnailBitmap = bm; + RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(mIconView.getResources(), bm); + drawable.setCornerRadius(mCornerDimensionCache * bm.getHeight()); + mIconView.setImageDrawable(drawable); + }; + mIconCache.getImage(mImageReceiver, mModItem.getIconCacheTag(), mModItem.imageUrl); + mSourceView.setImageResource(getSourceDrawable(item.apiSource)); + mTitle.setText(item.title); + mDescription.setText(item.description); + + if(hasExtended()){ + closeDetailedView(); + } + } + + /** Display extended info/interaction about a modpack */ + private void setStateDetailed(ModDetail detailedItem) { + if(detailedItem != null) { + setInstallEnabled(true); + mExtendedErrorTextView.setVisibility(View.GONE); + mVersionAdapter.setObjects(Arrays.asList(detailedItem.versionNames)); + mExtendedSpinner.setAdapter(mVersionAdapter); + } else { + closeDetailedView(); + setInstallEnabled(false); + mExtendedErrorTextView.setVisibility(View.VISIBLE); + mExtendedSpinner.setAdapter(null); + mVersionAdapter.setObjects(null); + } + } + + private void openDetailedView() { + mExtendedLayout.setVisibility(View.VISIBLE); + mDescription.setMaxLines(99); + + // We need to align to the longer section + int futureBottom = mDescription.getBottom() + Tools.mesureTextviewHeight(mDescription) - mDescription.getHeight(); + ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) mExtendedLayout.getLayoutParams(); + params.topToBottom = futureBottom > mIconView.getBottom() ? R.id.mod_body_textview : R.id.mod_thumbnail_imageview; + mExtendedLayout.setLayoutParams(params); + } + + private void closeDetailedView(){ + mExtendedLayout.setVisibility(View.GONE); + mDescription.setMaxLines(3); + } + + private void setDetailedStateDefault() { + setInstallEnabled(false); + mExtendedSpinner.setAdapter(mLoadingAdapter); + mExtendedErrorTextView.setVisibility(View.GONE); + openDetailedView(); + } + + private boolean hasExtended(){ + return mExtendedLayout != null; + } + + private boolean isExtended(){ + return hasExtended() && mExtendedLayout.getVisibility() == View.VISIBLE; + } + + private int getSourceDrawable(int apiSource) { + switch (apiSource) { + case Constants.SOURCE_CURSEFORGE: + return R.drawable.ic_curseforge; + case Constants.SOURCE_MODRINTH: + return R.drawable.ic_modrinth; + default: + throw new RuntimeException("Unknown API source"); + } + } + + private void setInstallEnabled(boolean enabled) { + mInstallEnabled = enabled; + updateInstallButtonState(); + } + + private void updateInstallButtonState() { + if(mExtendedButton != null) + mExtendedButton.setEnabled(mInstallEnabled && !mTasksRunning); + } + } + + /** + * The view holder used to hold the progress bar at the end of the list + */ + private static class LoadingViewHolder extends RecyclerView.ViewHolder { + public LoadingViewHolder(View view) { + super(view); + } + } + + private class SearchApiTask implements SelfReferencingFuture.FutureInterface { + private final SearchFilters mSearchFilters; + private final SearchResult mPreviousResult; + + private SearchApiTask(SearchFilters searchFilters, SearchResult previousResult) { + this.mSearchFilters = searchFilters; + this.mPreviousResult = previousResult; + } + + @SuppressLint("NotifyDataSetChanged") + @Override + public void run(Future myFuture) { + SearchResult result = mModpackApi.searchMod(mSearchFilters, mPreviousResult); + ModItem[] resultModItems = result != null ? result.results : null; + if(resultModItems != null && resultModItems.length != 0 && mPreviousResult != null) { + ModItem[] newModItems = new ModItem[resultModItems.length + mModItems.length]; + System.arraycopy(mModItems, 0, newModItems, 0, mModItems.length); + System.arraycopy(resultModItems, 0, newModItems, mModItems.length, resultModItems.length); + resultModItems = newModItems; + } + ModItem[] finalModItems = resultModItems; + Tools.runOnUiThread(() -> { + if(myFuture.isCancelled()) return; + mTaskInProgress = null; + if(finalModItems == null) { + mSearchResultCallback.onSearchError(SearchResultCallback.ERROR_INTERNAL); + }else if(finalModItems.length == 0) { + if(mPreviousResult != null) { + mLastPage = true; + notifyItemChanged(mModItems.length); + mSearchResultCallback.onSearchFinished(); + return; + } + mSearchResultCallback.onSearchError(SearchResultCallback.ERROR_NO_RESULTS); + }else{ + mSearchResultCallback.onSearchFinished(); + } + mCurrentResult = result; + if(finalModItems == null) { + mModItems = MOD_ITEMS_EMPTY; + notifyDataSetChanged(); + return; + } + if(mPreviousResult != null) { + int prevLength = mModItems.length; + mModItems = finalModItems; + notifyItemChanged(prevLength); + notifyItemRangeInserted(prevLength+1, mModItems.length); + }else { + mModItems = finalModItems; + notifyDataSetChanged(); + } + }); + } + } + + public interface SearchResultCallback { + int ERROR_INTERNAL = 0; + int ERROR_NO_RESULTS = 1; + void onSearchFinished(); + void onSearchError(int error); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModloaderInstallTracker.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModloaderInstallTracker.java new file mode 100644 index 000000000..8ec11e3fa --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModloaderInstallTracker.java @@ -0,0 +1,106 @@ +package net.kdt.pojavlaunch.modloaders.modpacks; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +import net.kdt.pojavlaunch.modloaders.modpacks.api.ModLoader; + +import java.io.File; + +/** + * This class is meant to track the availability of a modloader that is ready to be installed (as a result of modpack installation) + * It is needed because having all this logic spread over LauncherActivity would be clumsy, and I think that this is the best way to + * ensure that the modloader installer will run, even if the user does not receive the notification or something else happens + */ +public class ModloaderInstallTracker implements SharedPreferences.OnSharedPreferenceChangeListener { + private final SharedPreferences mSharedPreferences; + private final Activity mActivity; + + /** + * Create a ModInstallTracker object. This must be done in the Activity's onCreate method. + * @param activity the host activity + */ + public ModloaderInstallTracker(Activity activity) { + mActivity = activity; + mSharedPreferences = getPreferences(activity); + + } + + /** + * Attach the ModloaderInstallTracker to the current Activity. Must be done in the Activity's + * onResume method + */ + public void attach() { + mSharedPreferences.registerOnSharedPreferenceChangeListener(this); + runCheck(); + } + + /** + * Detach the ModloaderInstallTracker from the current Activity. Must be done in the Activity's + * onPause method + */ + public void detach() { + mSharedPreferences.unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String prefName) { + if(!"modLoaderAvailable".equals(prefName)) return; + runCheck(); + } + + @SuppressLint("ApplySharedPref") + private void runCheck() { + if(!mSharedPreferences.getBoolean("modLoaderAvailable", false)) return; + SharedPreferences.Editor editor = mSharedPreferences.edit().putBoolean("modLoaderAvailable", false); + if(!editor.commit()) editor.apply(); + ModLoader modLoader = deserializeModLoader(mSharedPreferences); + File modInstallFile = deserializeInstallFile(mSharedPreferences); + if(modLoader == null || modInstallFile == null) return; + startModInstallation(modLoader, modInstallFile); + } + + private void startModInstallation(ModLoader modLoader, File modInstallFile) { + Intent installIntent = modLoader.getInstallationIntent(mActivity, modInstallFile); + mActivity.startActivity(installIntent); + } + + private static SharedPreferences getPreferences(Context context) { + return context.getSharedPreferences("modloader_info", Context.MODE_PRIVATE); + } + + /** + * Store the data necessary to start a ModLoader installation for the tracker to start the installer + * sometime. + * @param context the Context + * @param modLoader the ModLoader to store + * @param modInstallFile the installer jar to store + */ + @SuppressLint("ApplySharedPref") + public static void saveModLoader(Context context, ModLoader modLoader, File modInstallFile) { + SharedPreferences.Editor editor = getPreferences(context).edit(); + editor.putInt("modLoaderType", modLoader.modLoaderType); + editor.putString("modLoaderVersion", modLoader.modLoaderVersion); + editor.putString("minecraftVersion", modLoader.minecraftVersion); + editor.putString("modInstallerJar", modInstallFile.getAbsolutePath()); + editor.putBoolean("modLoaderAvailable", true); + editor.commit(); + } + + private static ModLoader deserializeModLoader(SharedPreferences sharedPreferences) { + if(!sharedPreferences.contains("modLoaderType") || + !sharedPreferences.contains("modLoaderVersion") || + !sharedPreferences.contains("minecraftVersion")) return null; + return new ModLoader(sharedPreferences.getInt("modLoaderType", -1), + sharedPreferences.getString("modLoaderVersion", ""), + sharedPreferences.getString("minecraftVersion", "")); + } + + private static File deserializeInstallFile(SharedPreferences sharedPreferences) { + if(!sharedPreferences.contains("modInstallerJar")) return null; + return new File(sharedPreferences.getString("modInstallerJar", "")); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/SelfReferencingFuture.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/SelfReferencingFuture.java new file mode 100644 index 000000000..6f7d625af --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/SelfReferencingFuture.java @@ -0,0 +1,40 @@ +package net.kdt.pojavlaunch.modloaders.modpacks; + +import android.util.Log; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +public class SelfReferencingFuture { + private final Object mFutureLock = new Object(); + private final FutureInterface mFutureInterface; + private Future mMyFuture; + + public SelfReferencingFuture(FutureInterface futureInterface) { + this.mFutureInterface = futureInterface; + } + + public Future startOnExecutor(ExecutorService executorService) { + Future future = executorService.submit(this::run); + synchronized (mFutureLock) { + mMyFuture = future; + mFutureLock.notify(); + } + return future; + } + + private void run() { + try { + synchronized (mFutureLock) { + if (mMyFuture == null) mFutureLock.wait(); + } + mFutureInterface.run(mMyFuture); + }catch (InterruptedException e) { + Log.i("SelfReferencingFuture", "Interrupted while acquiring own Future"); + } + } + + public interface FutureInterface { + void run(Future myFuture); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ApiHandler.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ApiHandler.java new file mode 100644 index 000000000..4c03ecf2b --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ApiHandler.java @@ -0,0 +1,164 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import android.util.ArrayMap; +import android.util.Log; + +import com.google.gson.Gson; + +import net.kdt.pojavlaunch.Tools; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@SuppressWarnings("unused") +public class ApiHandler { + public final String baseUrl; + public final Map additionalHeaders; + + public ApiHandler(String url) { + baseUrl = url; + additionalHeaders = null; + } + + public ApiHandler(String url, String apiKey) { + baseUrl = url; + additionalHeaders = new ArrayMap<>(); + additionalHeaders.put("x-api-key", apiKey); + } + + public T get(String endpoint, Class tClass) { + return getFullUrl(additionalHeaders, baseUrl + "/" + endpoint, tClass); + } + + public T get(String endpoint, HashMap query, Class tClass) { + return getFullUrl(additionalHeaders, baseUrl + "/" + endpoint, query, tClass); + } + + public T post(String endpoint, T body, Class tClass) { + return postFullUrl(additionalHeaders, baseUrl + "/" + endpoint, body, tClass); + } + + public T post(String endpoint, HashMap query, T body, Class tClass) { + return postFullUrl(additionalHeaders, baseUrl + "/" + endpoint, query, body, tClass); + } + + //Make a get request and return the response as a raw string; + public static String getRaw(String url) { + return getRaw(null, url); + } + + public static String getRaw(Map headers, String url) { + Log.d("ApiHandler", url); + try { + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + addHeaders(conn, headers); + InputStream inputStream = conn.getInputStream(); + String data = Tools.read(inputStream); + Log.d(ApiHandler.class.toString(), data); + inputStream.close(); + conn.disconnect(); + return data; + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + public static String postRaw(String url, String body) { + return postRaw(null, url, body); + } + + public static String postRaw(Map headers, String url, String body) { + try { + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Accept", "application/json"); + addHeaders(conn, headers); + conn.setDoOutput(true); + + OutputStream outputStream = conn.getOutputStream(); + byte[] input = body.getBytes(StandardCharsets.UTF_8); + outputStream.write(input, 0, input.length); + outputStream.close(); + + InputStream inputStream = conn.getInputStream(); + String data = Tools.read(inputStream); + inputStream.close(); + + conn.disconnect(); + return data; + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + private static void addHeaders(HttpURLConnection connection, Map headers) { + if(headers != null) { + for(String key : headers.keySet()) + connection.addRequestProperty(key, headers.get(key)); + } + } + + private static String parseQueries(HashMap query) { + StringBuilder params = new StringBuilder("?"); + for (String param : query.keySet()) { + String value = Objects.toString(query.get(param)); + params.append(urlEncodeUTF8(param)) + .append("=") + .append(urlEncodeUTF8(value)) + .append("&"); + } + return params.substring(0, params.length() - 1); + } + + public static T getFullUrl(String url, Class tClass) { + return getFullUrl(null, url, tClass); + } + + public static T getFullUrl(String url, HashMap query, Class tClass) { + return getFullUrl(null, url, query, tClass); + } + + public static T postFullUrl(String url, T body, Class tClass) { + return postFullUrl(null, url, body, tClass); + } + + public static T postFullUrl(String url, HashMap query, T body, Class tClass) { + return postFullUrl(null, url, query, body, tClass); + } + + public static T getFullUrl(Map headers, String url, Class tClass) { + return new Gson().fromJson(getRaw(headers, url), tClass); + } + + public static T getFullUrl(Map headers, String url, HashMap query, Class tClass) { + return getFullUrl(headers, url + parseQueries(query), tClass); + } + + public static T postFullUrl(Map headers, String url, T body, Class tClass) { + return new Gson().fromJson(postRaw(headers, url, body.toString()), tClass); + } + + public static T postFullUrl(Map headers, String url, HashMap query, T body, Class tClass) { + return new Gson().fromJson(postRaw(headers, url + parseQueries(query), body.toString()), tClass); + } + + private static String urlEncodeUTF8(String input) { + try { + return URLEncoder.encode(input, "UTF-8"); + }catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 is required"); + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java new file mode 100644 index 000000000..d7aaef318 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java @@ -0,0 +1,187 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import androidx.annotation.NonNull; + +import net.kdt.pojavlaunch.PojavApplication; +import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; + +/** + * Group all apis under the same umbrella, as another layer of abstraction + */ +public class CommonApi implements ModpackApi { + + private final ModpackApi mCurseforgeApi; + private final ModpackApi mModrinthApi; + private final ModpackApi[] mModpackApis; + + public CommonApi(String curseforgeApiKey) { + mCurseforgeApi = new CurseforgeApi(curseforgeApiKey); + mModrinthApi = new ModrinthApi(); + mModpackApis = new ModpackApi[]{mModrinthApi, mCurseforgeApi}; + } + + @Override + public SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult) { + CommonApiSearchResult commonApiSearchResult = (CommonApiSearchResult) previousPageResult; + // If there are no previous page results, create a new array. Otherwise, use the one from the previous page + SearchResult[] results = commonApiSearchResult == null ? + new SearchResult[mModpackApis.length] : commonApiSearchResult.searchResults; + + int totalSize = 0; + int totalTotalSize = 0; + + Future[] futures = new Future[mModpackApis.length]; + for(int i = 0; i < mModpackApis.length; i++) { + // If there is an array and its length is zero, this means that we've exhausted the results for this + // search query and we don't need to actually do the search + if(results[i] != null && results[i].results.length == 0) continue; + // If the previous page result is not null (aka the arrays aren't fresh) + // and the previous result is null, it means that na error has occured on the previous + // page. We lost contingency anyway, so don't bother requesting. + if(previousPageResult != null && results[i] == null) continue; + futures[i] = PojavApplication.sExecutorService.submit(new ApiDownloadTask(i, searchFilters, + results[i])); + } + + if(Thread.interrupted()) { + cancelAllFutures(futures); + return null; + } + boolean hasSuccessful = false; + // Count up all the results + for(int i = 0; i < mModpackApis.length; i++) { + Future future = futures[i]; + if(future == null) continue; + try { + SearchResult searchResult = results[i] = (SearchResult) future.get(); + if(searchResult != null) hasSuccessful = true; + else continue; + totalSize += searchResult.results.length; + totalTotalSize += searchResult.totalResultCount; + }catch (Exception e) { + cancelAllFutures(futures); + e.printStackTrace(); + return null; + } + } + if(!hasSuccessful) { + return null; + } + // Then build an array with all the mods + ArrayList filteredResults = new ArrayList<>(results.length); + + // Sanitize returned values + for(SearchResult result : results) { + if(result == null) continue; + ModItem[] searchResults = result.results; + // If the length is zero, we don't need to perform needless copies + if(searchResults.length == 0) continue; + filteredResults.add(searchResults); + } + filteredResults.trimToSize(); + if(Thread.interrupted()) return null; + + ModItem[] concatenatedItems = buildFusedResponse(filteredResults); + if(Thread.interrupted()) return null; + // Recycle or create new search result + if(commonApiSearchResult == null) commonApiSearchResult = new CommonApiSearchResult(); + commonApiSearchResult.searchResults = results; + commonApiSearchResult.totalResultCount = totalTotalSize; + commonApiSearchResult.results = concatenatedItems; + return commonApiSearchResult; + } + + @Override + public ModDetail getModDetails(ModItem item) { + return getModpackApi(item.apiSource).getModDetails(item); + } + + @Override + public ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException { + return getModpackApi(modDetail.apiSource).installMod(modDetail, selectedVersion); + } + + private @NonNull ModpackApi getModpackApi(int apiSource) { + switch (apiSource) { + case Constants.SOURCE_MODRINTH: + return mModrinthApi; + case Constants.SOURCE_CURSEFORGE: + return mCurseforgeApi; + default: + throw new UnsupportedOperationException("Unknown API source: " + apiSource); + } + } + + /** Fuse the arrays in a way that's fair for every endpoint */ + private ModItem[] buildFusedResponse(List modMatrix){ + int totalSize = 0; + + // Calculate the total size of the merged array + for (ModItem[] array : modMatrix) { + totalSize += array.length; + } + + ModItem[] fusedItems = new ModItem[totalSize]; + + int mergedIndex = 0; + int maxLength = 0; + + // Find the maximum length of arrays + for (ModItem[] array : modMatrix) { + if (array.length > maxLength) { + maxLength = array.length; + } + } + + // Populate the merged array + for (int i = 0; i < maxLength; i++) { + for (ModItem[] matrix : modMatrix) { + if (i < matrix.length) { + fusedItems[mergedIndex] = matrix[i]; + mergedIndex++; + } + } + } + + return fusedItems; + } + + private void cancelAllFutures(Future[] futures) { + for(Future future : futures) { + if(future == null) continue; + future.cancel(true); + } + } + + private class ApiDownloadTask implements Callable { + private final int mModApi; + private final SearchFilters mSearchFilters; + private final SearchResult mPreviousPageResult; + + private ApiDownloadTask(int modApi, SearchFilters searchFilters, SearchResult previousPageResult) { + this.mModApi = modApi; + this.mSearchFilters = searchFilters; + this.mPreviousPageResult = previousPageResult; + } + + @Override + public SearchResult call() { + return mModpackApis[mModApi].searchMod(mSearchFilters, mPreviousPageResult); + } + } + + class CommonApiSearchResult extends SearchResult { + SearchResult[] searchResults = new SearchResult[mModpackApis.length]; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java new file mode 100644 index 000000000..0920509bf --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java @@ -0,0 +1,242 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import android.content.Context; +import android.util.Log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.kdt.mcgui.ProgressLayout; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants; +import net.kdt.pojavlaunch.modloaders.modpacks.models.CurseManifest; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult; +import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; +import net.kdt.pojavlaunch.utils.FileUtils; +import net.kdt.pojavlaunch.utils.ZipUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.regex.Pattern; +import java.util.zip.ZipFile; + +public class CurseforgeApi implements ModpackApi{ + private static final Pattern sMcVersionPattern = Pattern.compile("([0-9]+)\\.([0-9]+)\\.?([0-9]+)?"); + // Stolen from + // https://github.com/AnzhiZhang/CurseForgeModpackDownloader/blob/6cb3f428459f0cc8f444d16e54aea4cd1186fd7b/utils/requester.py#L93 + private static final int CURSEFORGE_MINECRAFT_GAME_ID = 432; + private static final int CURSEFORGE_MODPACK_CLASS_ID = 4471; + // https://api.curseforge.com/v1/categories?gameId=432 and search for "Mods" (case-sensitive) + private static final int CURSEFORGE_MOD_CLASS_ID = 6; + private static final int CURSEFORGE_SORT_RELEVANCY = 1; + private static final int CURSEFORGE_PAGINATION_SIZE = 50; + private static final int CURSEFORGE_PAGINATION_END_REACHED = -1; + private static final int CURSEFORGE_PAGINATION_ERROR = -2; + + private final ApiHandler mApiHandler; + public CurseforgeApi(String apiKey) { + mApiHandler = new ApiHandler("https://api.curseforge.com/v1", apiKey); + } + + @Override + public SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult) { + CurseforgeSearchResult curseforgeSearchResult = (CurseforgeSearchResult) previousPageResult; + + HashMap params = new HashMap<>(); + params.put("gameId", CURSEFORGE_MINECRAFT_GAME_ID); + params.put("classId", searchFilters.isModpack ? CURSEFORGE_MODPACK_CLASS_ID : CURSEFORGE_MOD_CLASS_ID); + params.put("searchFilter", searchFilters.name); + params.put("sortField", CURSEFORGE_SORT_RELEVANCY); + params.put("sortOrder", "desc"); + if(searchFilters.mcVersion != null && !searchFilters.mcVersion.isEmpty()) + params.put("gameVersion", searchFilters.mcVersion); + if(previousPageResult != null) + params.put("index", curseforgeSearchResult.previousOffset); + + JsonObject response = mApiHandler.get("mods/search", params, JsonObject.class); + if(response == null) return null; + JsonArray dataArray = response.getAsJsonArray("data"); + if(dataArray == null) return null; + JsonObject paginationInfo = response.getAsJsonObject("pagination"); + ArrayList modItemList = new ArrayList<>(dataArray.size()); + for(int i = 0; i < dataArray.size(); i++) { + JsonObject dataElement = dataArray.get(i).getAsJsonObject(); + JsonElement allowModDistribution = dataElement.get("allowModDistribution"); + // Gson automatically casts null to false, which leans to issues + // So, only check the distribution flag if it is non-null + if(!allowModDistribution.isJsonNull() && !allowModDistribution.getAsBoolean()) { + Log.i("CurseforgeApi", "Skipping modpack "+dataElement.get("name").getAsString() + " because curseforge sucks"); + continue; + } + ModItem modItem = new ModItem(Constants.SOURCE_CURSEFORGE, + searchFilters.isModpack, + dataElement.get("id").getAsString(), + dataElement.get("name").getAsString(), + dataElement.get("summary").getAsString(), + dataElement.getAsJsonObject("logo").get("thumbnailUrl").getAsString()); + modItemList.add(modItem); + } + if(curseforgeSearchResult == null) curseforgeSearchResult = new CurseforgeSearchResult(); + curseforgeSearchResult.results = modItemList.toArray(new ModItem[0]); + curseforgeSearchResult.totalResultCount = paginationInfo.get("totalCount").getAsInt(); + curseforgeSearchResult.previousOffset += dataArray.size(); + return curseforgeSearchResult; + + } + + @Override + public ModDetail getModDetails(ModItem item) { + ArrayList allModDetails = new ArrayList<>(); + int index = 0; + while(index != CURSEFORGE_PAGINATION_END_REACHED && + index != CURSEFORGE_PAGINATION_ERROR) { + index = getPaginatedDetails(allModDetails, index, item.id); + } + if(index == CURSEFORGE_PAGINATION_ERROR) return null; + int length = allModDetails.size(); + String[] versionNames = new String[length]; + String[] mcVersionNames = new String[length]; + String[] versionUrls = new String[length]; + for(int i = 0; i < allModDetails.size(); i++) { + JsonObject modDetail = allModDetails.get(i); + versionNames[i] = modDetail.get("displayName").getAsString(); + JsonElement downloadUrl = modDetail.get("downloadUrl"); + versionUrls[i] = downloadUrl.getAsString(); + JsonArray gameVersions = modDetail.getAsJsonArray("gameVersions"); + for(JsonElement jsonElement : gameVersions) { + String gameVersion = jsonElement.getAsString(); + if(!sMcVersionPattern.matcher(gameVersion).matches()) { + continue; + } + mcVersionNames[i] = gameVersion; + break; + } + } + return new ModDetail(item, versionNames, mcVersionNames, versionUrls); + } + + @Override + public ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException{ + //TODO considering only modpacks for now + return ModpackInstaller.installModpack(modDetail, selectedVersion, this::installCurseforgeZip); + } + + + private int getPaginatedDetails(ArrayList objectList, int index, String modId) { + HashMap params = new HashMap<>(); + params.put("index", index); + params.put("pageSize", CURSEFORGE_PAGINATION_SIZE); + + JsonObject response = mApiHandler.get("mods/"+modId+"/files", params, JsonObject.class); + if(response == null) return CURSEFORGE_PAGINATION_ERROR; + JsonArray data = response.getAsJsonArray("data"); + if(data == null) return CURSEFORGE_PAGINATION_ERROR; + for(int i = 0; i < data.size(); i++) { + JsonObject fileInfo = data.get(i).getAsJsonObject(); + if(fileInfo.get("isServerPack").getAsBoolean()) continue; + objectList.add(fileInfo); + } + if(data.size() < CURSEFORGE_PAGINATION_SIZE) { + return CURSEFORGE_PAGINATION_END_REACHED; // we read the remainder! yay! + } + return index + data.size(); + } + + private ModLoader installCurseforgeZip(File zipFile, File instanceDestination) throws IOException { + try (ZipFile modpackZipFile = new ZipFile(zipFile)){ + CurseManifest curseManifest = Tools.GLOBAL_GSON.fromJson( + Tools.read(ZipUtils.getEntryStream(modpackZipFile, "manifest.json")), + CurseManifest.class); + if(!verifyManifest(curseManifest)) { + Log.i("CurseforgeApi","manifest verification failed"); + return null; + } + ModDownloader modDownloader = new ModDownloader(new File(instanceDestination,"mods"), true); + int fileCount = curseManifest.files.length; + for(int i = 0; i < fileCount; i++) { + final CurseManifest.CurseFile curseFile = curseManifest.files[i]; + modDownloader.submitDownload(()->{ + String url = getDownloadUrl(curseFile.projectID, curseFile.fileID); + if(url == null && curseFile.required) + throw new IOException("Failed to obtain download URL for "+curseFile.projectID+" "+curseFile.fileID); + else if(url == null) return null; + return new ModDownloader.FileInfo(url, FileUtils.getFileName(url)); + }); + } + modDownloader.awaitFinish((c,m)-> + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, (int) Math.max((float)c/m*100,0), R.string.modpack_download_downloading_mods_fc, c, m) + ); + String overridesDir = "overrides"; + if(curseManifest.overrides != null) overridesDir = curseManifest.overrides; + ZipUtils.zipExtract(modpackZipFile, overridesDir, instanceDestination); + return createInfo(curseManifest.minecraft); + } + } + + private ModLoader createInfo(CurseManifest.CurseMinecraft minecraft) { + CurseManifest.CurseModLoader primaryModLoader = null; + for(CurseManifest.CurseModLoader modLoader : minecraft.modLoaders) { + if(modLoader.primary) { + primaryModLoader = modLoader; + break; + } + } + if(primaryModLoader == null) primaryModLoader = minecraft.modLoaders[0]; + String modLoaderId = primaryModLoader.id; + int dashIndex = modLoaderId.indexOf('-'); + String modLoaderName = modLoaderId.substring(0, dashIndex); + String modLoaderVersion = modLoaderId.substring(dashIndex+1); + Log.i("CurseforgeApi", modLoaderId + " " + modLoaderName + " "+modLoaderVersion); + int modLoaderTypeInt; + switch (modLoaderName) { + case "forge": + modLoaderTypeInt = ModLoader.MOD_LOADER_FORGE; + break; + case "fabric": + modLoaderTypeInt = ModLoader.MOD_LOADER_FABRIC; + break; + default: + return null; + //TODO: Quilt is also Forge? How does that work? + } + return new ModLoader(modLoaderTypeInt, modLoaderVersion, minecraft.version); + } + + private String getDownloadUrl(long projectID, long fileID) { + // First try the official api endpoint + JsonObject response = mApiHandler.get("mods/"+projectID+"/files/"+fileID+"/download-url", JsonObject.class); + if (response != null && !response.get("data").isJsonNull()) + return response.get("data").getAsString(); + + // Otherwise, fallback to building an edge link + JsonObject fallbackResponse = mApiHandler.get(String.format("mods/%s/files/%s", projectID, fileID), JsonObject.class); + if (fallbackResponse != null && !fallbackResponse.get("data").isJsonNull()){ + JsonObject modData = fallbackResponse.get("data").getAsJsonObject(); + int id = modData.get("id").getAsInt(); + return String.format("https://edge.forgecdn.net/files/%s/%s/%s", id/1000, id % 1000, modData.get("fileName").getAsString()); + } + + return null; + } + + private boolean verifyManifest(CurseManifest manifest) { + if(!"minecraftModpack".equals(manifest.manifestType)) return false; + if(manifest.manifestVersion != 1) return false; + if(manifest.minecraft == null) return false; + if(manifest.minecraft.version == null) return false; + if(manifest.minecraft.modLoaders == null) return false; + if(manifest.minecraft.modLoaders.length < 1) return false; + return true; + } + + class CurseforgeSearchResult extends SearchResult { + int previousOffset; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModDownloader.java new file mode 100644 index 000000000..09ddd7c6e --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModDownloader.java @@ -0,0 +1,172 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.utils.DownloadUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +public class ModDownloader { + private static final ThreadLocal sThreadLocalBuffer = new ThreadLocal<>(); + private final ThreadPoolExecutor mDownloadPool = new ThreadPoolExecutor(4,4,100, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>()); + private final AtomicBoolean mTerminator = new AtomicBoolean(false); + private final AtomicLong mDownloadSize = new AtomicLong(0); + private final Object mExceptionSyncPoint = new Object(); + private final File mDestinationDirectory; + private final boolean mUseFileCount; + private IOException mFirstIOException; + private long mTotalSize; + + public ModDownloader(File destinationDirectory) { + this(destinationDirectory, false); + } + + public ModDownloader(File destinationDirectory, boolean useFileCount) { + this.mDownloadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); + this.mDestinationDirectory = destinationDirectory; + this.mUseFileCount = useFileCount; + } + + public void submitDownload(int fileSize, String relativePath, String... url) { + if(mUseFileCount) mTotalSize += 1; + else mTotalSize += fileSize; + mDownloadPool.execute(new DownloadTask(url, new File(mDestinationDirectory, relativePath))); + } + + public void submitDownload(FileInfoProvider infoProvider) { + if(!mUseFileCount) throw new RuntimeException("This method can only be used in a file-counting ModDownloader"); + mTotalSize += 1; + mDownloadPool.execute(new FileInfoQueryTask(infoProvider)); + } + + public void awaitFinish(Tools.DownloaderFeedback feedback) throws IOException { + try { + mDownloadPool.shutdown(); + while(!mDownloadPool.awaitTermination(20, TimeUnit.MILLISECONDS) && !mTerminator.get()) { + feedback.updateProgress((int) mDownloadSize.get(), (int) mTotalSize); + } + if(mTerminator.get()) { + mDownloadPool.shutdownNow(); + synchronized (mExceptionSyncPoint) { + if(mFirstIOException == null) mExceptionSyncPoint.wait(); + throw mFirstIOException; + } + } + }catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private static byte[] getThreadLocalBuffer() { + byte[] buffer = sThreadLocalBuffer.get(); + if(buffer != null) return buffer; + buffer = new byte[8192]; + sThreadLocalBuffer.set(buffer); + return buffer; + } + + private void downloadFailed(IOException exception) { + mTerminator.set(true); + synchronized (mExceptionSyncPoint) { + if(mFirstIOException == null) { + mFirstIOException = exception; + mExceptionSyncPoint.notify(); + } + } + } + + class FileInfoQueryTask implements Runnable { + private final FileInfoProvider mFileInfoProvider; + public FileInfoQueryTask(FileInfoProvider fileInfoProvider) { + this.mFileInfoProvider = fileInfoProvider; + } + @Override + public void run() { + try { + FileInfo fileInfo = mFileInfoProvider.getFileInfo(); + if(fileInfo == null) return; + new DownloadTask(new String[]{fileInfo.url}, + new File(mDestinationDirectory, fileInfo.relativePath)).run(); + }catch (IOException e) { + downloadFailed(e); + } + } + } + + class DownloadTask implements Runnable, Tools.DownloaderFeedback { + private final String[] mDownloadUrls; + private final File mDestination; + private int last = 0; + + public DownloadTask(String[] downloadurls, + File downloadDestination) { + this.mDownloadUrls = downloadurls; + this.mDestination = downloadDestination; + } + + @Override + public void run() { + IOException exception = null; + for(String sourceUrl : mDownloadUrls) { + try { + exception = tryDownload(sourceUrl); + if(exception == null) return; + }catch (InterruptedException e) { + return; + } + } + if(exception != null) { + downloadFailed(exception); + } + } + + private IOException tryDownload(String sourceUrl) throws InterruptedException { + IOException exception = null; + for (int i = 0; i < 5; i++) { + try { + DownloadUtils.downloadFileMonitored(sourceUrl, mDestination, getThreadLocalBuffer(), this); + if(mUseFileCount) mDownloadSize.addAndGet(1); + return null; + } catch (InterruptedIOException e) { + throw new InterruptedException(); + } catch (IOException e) { + e.printStackTrace(); + exception = e; + } + if(!mUseFileCount) { + mDownloadSize.addAndGet(-last); + last = 0; + } + } + return exception; + } + + @Override + public void updateProgress(int curr, int max) { + if(mUseFileCount) return; + mDownloadSize.addAndGet(curr - last); + last = curr; + } + } + + public static class FileInfo { + public final String url; + public final String relativePath; + + public FileInfo(String url, String relativePath) { + this.url = url; + this.relativePath = relativePath; + } + } + + public interface FileInfoProvider { + FileInfo getFileInfo() throws IOException; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoader.java new file mode 100644 index 000000000..1eef3567b --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoader.java @@ -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); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java new file mode 100644 index 000000000..141468af8 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java @@ -0,0 +1,73 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + + +import android.content.Context; + +import com.kdt.mcgui.ProgressLayout; + +import net.kdt.pojavlaunch.PojavApplication; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult; + +import java.io.IOException; + +/** + * + */ +public interface ModpackApi { + + /** + * @param searchFilters Filters + * @param previousPageResult The result from the previous page + * @return the list of mod items from specified offset + */ + SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult); + + /** + * @param searchFilters Filters + * @return A list of mod items + */ + default SearchResult searchMod(SearchFilters searchFilters) { + return searchMod(searchFilters, null); + } + + /** + * Fetch the mod details + * @param item The moditem that was selected + * @return Detailed data about a mod(pack) + */ + ModDetail getModDetails(ModItem item); + + /** + * Download and install the mod(pack) + * @param modDetail The mod detail data + * @param selectedVersion The selected version + */ + default void handleInstallation(Context context, ModDetail modDetail, int selectedVersion) { + // Doing this here since when starting installation, the progress does not start immediately + // which may lead to two concurrent installations (very bad) + ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.global_waiting); + PojavApplication.sExecutorService.execute(() -> { + try { + ModLoader loaderInfo = installMod(modDetail, selectedVersion); + if (loaderInfo == null) return; + loaderInfo.getDownloadTask(new NotificationDownloadListener(context, loaderInfo)).run(); + }catch (IOException e) { + Tools.showErrorRemote(context, R.string.modpack_install_download_failed, e); + } + }); + } + + /** + * Install the mod(pack). + * May require the download of additional files. + * May requires launching the installation of a modloader + * @param modDetail The mod detail data + * @param selectedVersion The selected version + */ + ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException; +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackInstaller.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackInstaller.java new file mode 100644 index 000000000..708754280 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackInstaller.java @@ -0,0 +1,63 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import com.kdt.mcgui.ProgressLayout; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener; +import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ModIconCache; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; +import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper; +import net.kdt.pojavlaunch.utils.DownloadUtils; +import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; +import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +public class ModpackInstaller { + + public static ModLoader installModpack(ModDetail modDetail, int selectedVersion, InstallFunction installFunction) throws IOException{ + String versionUrl = modDetail.versionUrls[selectedVersion]; + String modpackName = modDetail.title.toLowerCase(Locale.ROOT).trim().replace(" ", "_" ); + + // Build a new minecraft instance, folder first + + // Get the modpack file + File modpackFile = new File(Tools.DIR_CACHE, modpackName + ".cf"); // Cache File + ModLoader modLoaderInfo; + try { + byte[] downloadBuffer = new byte[8192]; + DownloadUtils.downloadFileMonitored(versionUrl, modpackFile, downloadBuffer, + new DownloaderProgressWrapper(R.string.modpack_download_downloading_metadata, + ProgressLayout.INSTALL_MODPACK)); + // Install the modpack + modLoaderInfo = installFunction.installModpack(modpackFile, new File(Tools.DIR_GAME_HOME, "custom_instances/"+modpackName)); + + } finally { + modpackFile.delete(); + ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); + } + if(modLoaderInfo == null) { + return null; + } + + // Create the instance + MinecraftProfile profile = new MinecraftProfile(); + profile.gameDir = "./custom_instances/" + modpackName; + profile.name = modDetail.title; + profile.lastVersionId = modLoaderInfo.getVersionId(); + profile.icon = ModIconCache.getBase64Image(modDetail.getIconCacheTag()); + + + LauncherProfiles.mainProfileJson.profiles.put(modpackName, profile); + LauncherProfiles.write(); + + return modLoaderInfo; + } + + interface InstallFunction { + ModLoader installModpack(File modpackFile, File instanceDestination) throws IOException; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java new file mode 100644 index 000000000..e8eded460 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java @@ -0,0 +1,139 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.kdt.mcgui.ProgressLayout; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModrinthIndex; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult; +import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper; +import net.kdt.pojavlaunch.utils.ZipUtils; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipFile; + +public class ModrinthApi implements ModpackApi{ + private final ApiHandler mApiHandler; + public ModrinthApi(){ + mApiHandler = new ApiHandler("https://api.modrinth.com/v2"); + } + + @Override + public SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult) { + ModrinthSearchResult modrinthSearchResult = (ModrinthSearchResult) previousPageResult; + HashMap params = new HashMap<>(); + + // Build the facets filters + StringBuilder facetString = new StringBuilder(); + facetString.append("["); + facetString.append(String.format("[\"project_type:%s\"]", searchFilters.isModpack ? "modpack" : "mod")); + if(searchFilters.mcVersion != null && !searchFilters.mcVersion.isEmpty()) + facetString.append(String.format(",[\"versions:%s\"]", searchFilters.mcVersion)); + facetString.append("]"); + params.put("facets", facetString.toString()); + params.put("query", searchFilters.name.replace(' ', '+')); + params.put("limit", 50); + params.put("index", "relevance"); + if(modrinthSearchResult != null) + params.put("offset", modrinthSearchResult.previousOffset); + + JsonObject response = mApiHandler.get("search", params, JsonObject.class); + if(response == null) return null; + JsonArray responseHits = response.getAsJsonArray("hits"); + if(responseHits == null) return null; + + ModItem[] items = new ModItem[responseHits.size()]; + for(int i=0; i dependencies = modrinthIndex.dependencies; + String mcVersion = dependencies.get("minecraft"); + if(mcVersion == null) return null; + String modLoaderVersion; + if((modLoaderVersion = dependencies.get("forge")) != null) { + return new ModLoader(ModLoader.MOD_LOADER_FORGE, modLoaderVersion, mcVersion); + } + if((modLoaderVersion = dependencies.get("fabric-loader")) != null) { + return new ModLoader(ModLoader.MOD_LOADER_FABRIC, modLoaderVersion, mcVersion); + } + if((modLoaderVersion = dependencies.get("quilt-loader")) != null) { + return new ModLoader(ModLoader.MOD_LOADER_QUILT, modLoaderVersion, mcVersion); + } + return null; + } + + private ModLoader installMrpack(File mrpackFile, File instanceDestination) throws IOException { + try (ZipFile modpackZipFile = new ZipFile(mrpackFile)){ + ModrinthIndex modrinthIndex = Tools.GLOBAL_GSON.fromJson( + Tools.read(ZipUtils.getEntryStream(modpackZipFile, "modrinth.index.json")), + ModrinthIndex.class); + + ModDownloader modDownloader = new ModDownloader(instanceDestination); + for(ModrinthIndex.ModrinthIndexFile indexFile : modrinthIndex.files) { + modDownloader.submitDownload(indexFile.fileSize, indexFile.path, indexFile.downloads); + } + modDownloader.awaitFinish(new DownloaderProgressWrapper(R.string.modpack_download_downloading_mods, ProgressLayout.INSTALL_MODPACK)); + ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.modpack_download_applying_overrides, 1, 2); + ZipUtils.zipExtract(modpackZipFile, "overrides/", instanceDestination); + ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 50, R.string.modpack_download_applying_overrides, 2, 2); + ZipUtils.zipExtract(modpackZipFile, "client-overrides/", instanceDestination); + return createInfo(modrinthIndex); + } + } + + class ModrinthSearchResult extends SearchResult { + int previousOffset; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/NotificationDownloadListener.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/NotificationDownloadListener.java new file mode 100644 index 000000000..84c41e961 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/NotificationDownloadListener.java @@ -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()); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/DownloadImageTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/DownloadImageTask.java new file mode 100644 index 000000000..9c9bdc942 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/DownloadImageTask.java @@ -0,0 +1,61 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.imagecache; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import net.kdt.pojavlaunch.utils.DownloadUtils; + +import java.io.FileOutputStream; +import java.io.IOException; + +class DownloadImageTask implements Runnable { + private static final float BITMAP_FINAL_DIMENSION = 256f; + private final ReadFromDiskTask mParentTask; + private int mRetryCount; + DownloadImageTask(ReadFromDiskTask parentTask) { + this.mParentTask = parentTask; + this.mRetryCount = 0; + } + + @Override + public void run() { + boolean wasSuccessful = false; + while(mRetryCount < 5 && !(wasSuccessful = runCatching())) { + mRetryCount++; + } + // restart the parent task to read the image and send it to the receiver + // if it wasn't cancelled. If it was, then we just die here + if(wasSuccessful && !mParentTask.taskCancelled()) + mParentTask.iconCache.cacheLoaderPool.execute(mParentTask); + } + + public boolean runCatching() { + try { + IconCacheJanitor.waitForJanitorToFinish(); + DownloadUtils.downloadFile(mParentTask.imageUrl, mParentTask.cacheFile); + Bitmap bitmap = BitmapFactory.decodeFile(mParentTask.cacheFile.getAbsolutePath()); + if(bitmap == null) return false; + int bitmapWidth = bitmap.getWidth(), bitmapHeight = bitmap.getHeight(); + if(bitmapWidth <= BITMAP_FINAL_DIMENSION && bitmapHeight <= BITMAP_FINAL_DIMENSION) { + bitmap.recycle(); + return true; + } + float imageRescaleRatio = Math.min(BITMAP_FINAL_DIMENSION/bitmapWidth, BITMAP_FINAL_DIMENSION/bitmapHeight); + Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, + (int)(bitmapWidth * imageRescaleRatio), + (int)(bitmapHeight * imageRescaleRatio), + true); + bitmap.recycle(); + if(resizedBitmap == bitmap) return true; + try (FileOutputStream fileOutputStream = new FileOutputStream(mParentTask.cacheFile)) { + resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 80, fileOutputStream); + } finally { + resizedBitmap.recycle(); + } + return true; + }catch (IOException e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/IconCacheJanitor.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/IconCacheJanitor.java new file mode 100644 index 000000000..f35fadeaa --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/IconCacheJanitor.java @@ -0,0 +1,86 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.imagecache; + +import android.util.Log; + +import net.kdt.pojavlaunch.PojavApplication; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** + * This image is intended to keep the mod icon cache tidy (aka under 100 megabytes) + */ +public class IconCacheJanitor implements Runnable{ + public static final long CACHE_SIZE_LIMIT = 104857600; // The cache size limit, 100 megabytes + public static final long CACHE_BRINGDOWN = 52428800; // The size to which the cache should be brought + // in case of an overflow, 50 mb + private static Future sJanitorFuture; + private static boolean sJanitorRan = false; + private IconCacheJanitor() { + // don't allow others to create this + } + @Override + public void run() { + File modIconCachePath = ModIconCache.getImageCachePath(); + if(!modIconCachePath.isDirectory() || !modIconCachePath.canRead()) return; + File[] modIconFiles = modIconCachePath.listFiles(); + if(modIconFiles == null) return; + ArrayList writableModIconFiles = new ArrayList<>(modIconFiles.length); + long directoryFileSize = 0; + for(File modIconFile : modIconFiles) { + if(!modIconFile.isFile() || !modIconFile.canRead()) continue; + directoryFileSize += modIconFile.length(); + if(!modIconFile.canWrite()) continue; + writableModIconFiles.add(modIconFile); + } + if(directoryFileSize < CACHE_SIZE_LIMIT) { + Log.i("IconCacheJanitor", "Skipping cleanup because there's not enough to clean up"); + return; + } + Arrays.sort(modIconFiles, + (x,y)-> Long.compare(y.lastModified(), x.lastModified()) + ); + int filesCleanedUp = 0; + for(File modFile : writableModIconFiles) { + if(directoryFileSize < CACHE_BRINGDOWN) break; + long modFileSize = modFile.length(); + if(modFile.delete()) { + directoryFileSize -= modFileSize; + filesCleanedUp++; + } + } + Log.i("IconCacheJanitor", "Cleaned up "+filesCleanedUp+ " files"); + synchronized (IconCacheJanitor.class) { + sJanitorFuture = null; + sJanitorRan = true; + } + } + + /** + * Runs the janitor task, unless there was one running already or one has ran already + */ + public static void runJanitor() { + synchronized (IconCacheJanitor.class) { + if (sJanitorFuture != null || sJanitorRan) return; + sJanitorFuture = PojavApplication.sExecutorService.submit(new IconCacheJanitor()); + } + } + + /** + * Waits for the janitor task to finish, if there is one running already + * Note that the thread waiting must not be interrupted. + */ + public static void waitForJanitorToFinish() { + synchronized (IconCacheJanitor.class) { + if (sJanitorFuture == null) return; + try { + sJanitorFuture.get(); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException("Should not happen!", e); + } + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ImageReceiver.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ImageReceiver.java new file mode 100644 index 000000000..f405b657c --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ImageReceiver.java @@ -0,0 +1,10 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.imagecache; + +import android.graphics.Bitmap; + +/** + * ModIconCache will call your view back when the image becomes available with this interface + */ +public interface ImageReceiver { + void onImageAvailable(Bitmap image); +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ModIconCache.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ModIconCache.java new file mode 100644 index 000000000..af1775633 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ModIconCache.java @@ -0,0 +1,109 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.imagecache; + +import android.util.Base64; +import android.util.Log; + +import net.kdt.pojavlaunch.Tools; + +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ModIconCache { + ThreadPoolExecutor cacheLoaderPool = new ThreadPoolExecutor(10, + 10, + 1000, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>()); + File cachePath; + private final List> mCancelledReceivers = new ArrayList<>(); + public ModIconCache() { + cachePath = getImageCachePath(); + if(!cachePath.exists() && !cachePath.isFile() && Tools.DIR_CACHE.canWrite()) { + if(!cachePath.mkdirs()) + throw new RuntimeException("Failed to create icon cache directory"); + } + + } + static File getImageCachePath() { + return new File(Tools.DIR_CACHE, "mod_icons"); + } + + /** + * Get an image for a mod with the associated tag and URL to download it in case if its not cached + * @param imageReceiver the receiver interface that would get called when the image loads + * @param imageTag the tag of the image to keep track of it + * @param imageUrl the URL of the image in case if it's not cached + */ + public void getImage(ImageReceiver imageReceiver, String imageTag, String imageUrl) { + cacheLoaderPool.execute(new ReadFromDiskTask(this, imageReceiver, imageTag, imageUrl)); + } + + /** + * Mark the image obtainment task requested with this receiver as "cancelled". This means that + * this receiver will not be called back and that some tasks related to this image may be + * prevented from happening or interrupted. + * @param imageReceiver the receiver to cancel + */ + public void cancelImage(ImageReceiver imageReceiver) { + synchronized (mCancelledReceivers) { + mCancelledReceivers.add(new WeakReference<>(imageReceiver)); + } + } + + boolean checkCancelled(ImageReceiver imageReceiver) { + boolean isCanceled = false; + synchronized (mCancelledReceivers) { + Iterator> iterator = mCancelledReceivers.iterator(); + while (iterator.hasNext()) { + WeakReference reference = iterator.next(); + if (reference.get() == null) { + iterator.remove(); + continue; + } + if(reference.get() == imageReceiver) { + isCanceled = true; + } + } + } + if(isCanceled) Log.i("IconCache", "checkCancelled("+imageReceiver.hashCode()+") == true"); + return isCanceled; + } + + /** + * Get the base64-encoded version of a cached icon by its tag. + * Note: this functions performs I/O operations, and should not be called on the UI + * thread. + * @param imageTag the icon tag + * @return the base64 encoded image or null if not cached + */ + + public static String getBase64Image(String imageTag) { + File imagePath = new File(Tools.DIR_CACHE, "mod_icons/"+imageTag+".ca"); + Log.i("IconCache", "Creating base64 version of icon "+imageTag); + if(!imagePath.canRead() || !imagePath.isFile()) { + Log.i("IconCache", "Icon does not exist"); + return null; + } + try { + try(FileInputStream fileInputStream = new FileInputStream(imagePath)) { + byte[] imageBytes = IOUtils.toByteArray(fileInputStream); + // reencode to png? who cares! our profile icon cache is an omnivore! + // if some other launcher parses this and dies it is not our problem :troll: + return "data:image/png;base64,"+ Base64.encodeToString(imageBytes, Base64.DEFAULT); + } + }catch (IOException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ReadFromDiskTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ReadFromDiskTask.java new file mode 100644 index 000000000..89f3ed41c --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ReadFromDiskTask.java @@ -0,0 +1,55 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.imagecache; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import net.kdt.pojavlaunch.Tools; + +import java.io.File; + +public class ReadFromDiskTask implements Runnable { + final ModIconCache iconCache; + final ImageReceiver imageReceiver; + final File cacheFile; + final String imageUrl; + + ReadFromDiskTask(ModIconCache iconCache, ImageReceiver imageReceiver, String cacheTag, String imageUrl) { + this.iconCache = iconCache; + this.imageReceiver = imageReceiver; + this.cacheFile = new File(iconCache.cachePath, cacheTag+".ca"); + this.imageUrl = imageUrl; + } + + public void runDownloadTask() { + iconCache.cacheLoaderPool.execute(new DownloadImageTask(this)); + } + + @Override + public void run() { + if(cacheFile.isDirectory()) { + return; + } + if(cacheFile.canRead()) { + IconCacheJanitor.waitForJanitorToFinish(); + Bitmap bitmap = BitmapFactory.decodeFile(cacheFile.getAbsolutePath()); + if(bitmap != null) { + Tools.runOnUiThread(()->{ + if(taskCancelled()) { + bitmap.recycle(); // do not leak the bitmap if the task got cancelled right at the end + return; + } + imageReceiver.onImageAvailable(bitmap); + }); + return; + } + } + if(iconCache.cachePath.canWrite() && + !taskCancelled()) { // don't run the download task if the task got canceled + runDownloadTask(); + } + } + @SuppressWarnings("BooleanMethodAlwaysInverted") + public boolean taskCancelled() { + return iconCache.checkCancelled(imageReceiver); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/Constants.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/Constants.java new file mode 100644 index 000000000..b628a2ae6 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/Constants.java @@ -0,0 +1,16 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.models; + +public class Constants { + private Constants(){} + + /** Types of modpack apis */ + public static final int SOURCE_MODRINTH = 0x0; + public static final int SOURCE_CURSEFORGE = 0x1; + public static final int SOURCE_TECHNIC = 0x2; + + /** Modrinth api, file environments */ + public static final String MODRINTH_FILE_ENV_REQUIRED = "required"; + public static final String MODRINTH_FILE_ENV_OPTIONAL = "optional"; + public static final String MODRINTH_FILE_ENV_UNSUPPORTED = "unsupported"; + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/CurseManifest.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/CurseManifest.java new file mode 100644 index 000000000..f7b82a4ca --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/CurseManifest.java @@ -0,0 +1,25 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.models; + +public class CurseManifest { + public String name; + public String version; + public String author; + public String manifestType; + public int manifestVersion; + public CurseFile[] files; + public CurseMinecraft minecraft; + public String overrides; + public static class CurseFile { + public long projectID; + public long fileID; + public boolean required; + } + public static class CurseMinecraft { + public String version; + public CurseModLoader[] modLoaders; + } + public static class CurseModLoader { + public String id; + public boolean primary; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java new file mode 100644 index 000000000..12f9ec5e1 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java @@ -0,0 +1,41 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.models; + + +import androidx.annotation.NonNull; + +import java.util.Arrays; + +public class ModDetail extends ModItem { + /* A cheap way to map from the front facing name to the underlying id */ + public String[] versionNames; + public String [] mcVersionNames; + public String[] versionUrls; + public ModDetail(ModItem item, String[] versionNames, String[] mcVersionNames, String[] versionUrls) { + super(item.apiSource, item.isModpack, item.id, item.title, item.description, item.imageUrl); + this.versionNames = versionNames; + this.mcVersionNames = mcVersionNames; + this.versionUrls = versionUrls; + + // Add the mc version to the version model + for (int i=0; i dependencies; + + + public static class ModrinthIndexFile { + public String path; + public String[] downloads; + public int fileSize; + + public ModrinthIndexFileHashes hashes; + + @Nullable public ModrinthIndexFileEnv env; + + @NonNull + @Override + public String toString() { + return "ModrinthIndexFile{" + + "path='" + path + '\'' + + ", downloads=" + Arrays.toString(downloads) + + ", fileSize=" + fileSize + + ", hashes=" + hashes + + '}'; + } + + public static class ModrinthIndexFileHashes { + public String sha1; + public String sha512; + + @NonNull + @Override + public String toString() { + return "ModrinthIndexFileHashes{" + + "sha1='" + sha1 + '\'' + + ", sha512='" + sha512 + '\'' + + '}'; + } + } + + public static class ModrinthIndexFileEnv { + public String client; + public String server; + + @NonNull + @Override + public String toString() { + return "ModrinthIndexFileEnv{" + + "client='" + client + '\'' + + ", server='" + server + '\'' + + '}'; + } + } + } + + @NonNull + @Override + public String toString() { + return "ModrinthIndex{" + + "formatVersion=" + formatVersion + + ", game='" + game + '\'' + + ", versionId='" + versionId + '\'' + + ", name='" + name + '\'' + + ", summary='" + summary + '\'' + + ", files=" + Arrays.toString(files) + + '}'; + } + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchFilters.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchFilters.java new file mode 100644 index 000000000..5694b3b1e --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchFilters.java @@ -0,0 +1,13 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.models; + +import org.jetbrains.annotations.Nullable; + +/** + * Search filters, passed to APIs + */ +public class SearchFilters { + public boolean isModpack; + public String name; + @Nullable public String mcVersion; + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchResult.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchResult.java new file mode 100644 index 000000000..94638435c --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchResult.java @@ -0,0 +1,6 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.models; + +public class SearchResult { + public int totalResultCount; + public ModItem[] results; +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/multirt/RTSpinnerAdapter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/multirt/RTSpinnerAdapter.java index e5548fceb..f7de37374 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/multirt/RTSpinnerAdapter.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/multirt/RTSpinnerAdapter.java @@ -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 ){ diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/ProfileAdapter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/ProfileAdapter.java index a3a9266da..367a73c95 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/ProfileAdapter.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/ProfileAdapter.java @@ -31,15 +31,10 @@ public class ProfileAdapter extends BaseAdapter { private Map mProfiles; private final MinecraftProfile dummy = new MinecraftProfile(); private List mProfileList; - private final ProfileAdapterExtra[] mExtraEntires; + private ProfileAdapterExtra[] mExtraEntires; - public ProfileAdapter(Context context, ProfileAdapterExtra[] extraEntries) { - ProfileIconCache.initDefault(context); - LauncherProfiles.update(); - mProfiles = new HashMap<>(LauncherProfiles.mainProfileJson.profiles); - if(extraEntries == null) mExtraEntires = new ProfileAdapterExtra[0]; - else mExtraEntires = extraEntries; - mProfileList = new ArrayList<>(Arrays.asList(mProfiles.keySet().toArray(new String[0]))); + 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(); + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/ProfileIconCache.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/ProfileIconCache.java index ccbd05470..e706ddcff 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/ProfileIconCache.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/ProfileIconCache.java @@ -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:;, + private static final String DATA_HEADER = "data:"; + private static final String FALLBACK_ICON_NAME = "default"; private static final Map sIconCache = new HashMap<>(); - private static Drawable sDefaultIcon; + private static final Map 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); } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/progresskeeper/DownloaderProgressWrapper.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/progresskeeper/DownloaderProgressWrapper.java new file mode 100644 index 000000000..bf2818186 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/progresskeeper/DownloaderProgressWrapper.java @@ -0,0 +1,40 @@ +package net.kdt.pojavlaunch.progresskeeper; + +import static net.kdt.pojavlaunch.Tools.BYTE_TO_MB; + +import net.kdt.pojavlaunch.Tools; + +public class DownloaderProgressWrapper implements Tools.DownloaderFeedback { + + private final int mProgressString; + private final String mProgressRecord; + public String extraString = null; + + /** + * A simple wrapper to send the downloader progress to ProgressKeeper + * @param progressString the string that will be used in the progress reporter + * @param progressRecord the record for ProgressKeeper + */ + public DownloaderProgressWrapper(int progressString, String progressRecord) { + this.mProgressString = progressString; + this.mProgressRecord = progressRecord; + } + + @Override + public void updateProgress(int curr, int max) { + Object[] va; + if(extraString != null) { + va = new Object[3]; + va[0] = extraString; + va[1] = curr/BYTE_TO_MB; + va[2] = max/BYTE_TO_MB; + } + else { + va = new Object[2]; + va[0] = curr/BYTE_TO_MB; + va[1] = max/BYTE_TO_MB; + } + // the allocations are fine because thats how java implements variadic arguments in bytecode: an array of whatever + ProgressKeeper.submitProgress(mProgressRecord, (int) Math.max((float)curr/max*100,0), mProgressString, va); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java index 00babeca1..a88416961 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java @@ -14,6 +14,7 @@ import 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 } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java index b5fc6396b..2ffa71eb9 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java @@ -19,6 +19,7 @@ import net.kdt.pojavlaunch.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); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java index 74112fefd..4de6935c3 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java @@ -1,6 +1,7 @@ package net.kdt.pojavlaunch.tasks; import static net.kdt.pojavlaunch.PojavApplication.sExecutorService; +import static net.kdt.pojavlaunch.Tools.BYTE_TO_MB; import static net.kdt.pojavlaunch.utils.DownloadUtils.downloadFileMonitored; import android.app.Activity; @@ -40,7 +41,7 @@ import java.util.concurrent.atomic.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 */ diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/FileUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/FileUtils.java index 44cc8c5fb..2256c6cc5 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/FileUtils.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/FileUtils.java @@ -1,9 +1,20 @@ package net.kdt.pojavlaunch.utils; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; public class FileUtils { public static boolean exists(String filePath){ return new File(filePath).exists(); } + + public static String getFileName(String pathOrUrl) { + int lastSlashIndex = pathOrUrl.lastIndexOf('/'); + if(lastSlashIndex == -1) return null; + return pathOrUrl.substring(lastSlashIndex); + } } \ No newline at end of file diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java index d17f12e3d..5dd996da4 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java @@ -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 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), diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/ZipUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/ZipUtils.java new file mode 100644 index 000000000..a56fd661b --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/ZipUtils.java @@ -0,0 +1,58 @@ +package net.kdt.pojavlaunch.utils; + +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class ZipUtils { + /** + * Gets an InputStream for a given ZIP entry, throwing an IOException if the ZIP entry does not + * exist. + * @param zipFile The ZipFile to get the entry from + * @param entryPath The full path inside of the ZipFile + * @return The InputStream provided by the ZipFile + * @throws IOException if the entry was not found + */ + public static InputStream getEntryStream(ZipFile zipFile, String entryPath) throws IOException{ + ZipEntry entry = zipFile.getEntry(entryPath); + if(entry == null) throw new IOException("No entry in ZIP file: "+entryPath); + return zipFile.getInputStream(entry); + } + + /** + * Extracts all files in a ZipFile inside of a given directory to a given destination directory + * How to specify dirName: + * If you want to extract all files in the ZipFile, specify "" + * If you want to extract a single directory, specify its full path followed by a trailing / + * @param zipFile The ZipFile to extract files from + * @param dirName The directory to extract the files from + * @param destination The destination directory to extract the files into + * @throws IOException if it was not possible to create a directory or file extraction failed + */ + public static void zipExtract(ZipFile zipFile, String dirName, File destination) throws IOException { + Enumeration zipEntries = zipFile.entries(); + + int dirNameLen = dirName.length(); + while(zipEntries.hasMoreElements()) { + ZipEntry zipEntry = zipEntries.nextElement(); + String entryName = zipEntry.getName(); + if(!entryName.startsWith(dirName) || zipEntry.isDirectory()) continue; + File zipDestination = new File(destination, entryName.substring(dirNameLen)); + File parent = zipDestination.getParentFile(); + if(parent != null && !parent.exists()) + if(!parent.mkdirs()) throw new IOException("Failed to create "+parent.getAbsolutePath()); + try (InputStream inputStream = zipFile.getInputStream(zipEntry); + OutputStream outputStream = + new FileOutputStream(zipDestination)) { + IOUtils.copy(inputStream, outputStream); + } + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/NotificationConstants.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/NotificationConstants.java new file mode 100644 index 000000000..02c23596e --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/NotificationConstants.java @@ -0,0 +1,12 @@ +package net.kdt.pojavlaunch.value; + +public class NotificationConstants { + public static final int NOTIFICATION_ID_PROGRESS_SERVICE = 1; + public static final int NOTIFICATION_ID_GAME_SERVICE = 2; + public static final int NOTIFICATION_ID_DOWNLOAD_LISTENER = 3; + public static final int NOTIFICATION_ID_SHOW_ERROR = 4; + public static final int PENDINGINTENT_CODE_KILL_PROGRESS_SERVICE = 1; + public static final int PENDINGINTENT_CODE_KILL_GAME_SERVICE = 2; + public static final int PENDINGINTENT_CODE_DOWNLOAD_SERVICE = 3; + public static final int PENDINGINTENT_CODE_SHOW_ERROR = 4; +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/launcherprofiles/LauncherProfiles.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/launcherprofiles/LauncherProfiles.java index 7d7f826d2..e13c9d83d 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/launcherprofiles/LauncherProfiles.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/launcherprofiles/LauncherProfiles.java @@ -8,45 +8,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 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; } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/launcherprofiles/MinecraftProfile.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/launcherprofiles/MinecraftProfile.java index 18b4d82d4..94545abf6 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/launcherprofiles/MinecraftProfile.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/launcherprofiles/MinecraftProfile.java @@ -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){ diff --git a/app_pojavlauncher/src/main/jni/egl_bridge.c b/app_pojavlauncher/src/main/jni/egl_bridge.c index 91f1e5b62..1e900eeaf 100644 --- a/app_pojavlauncher/src/main/jni/egl_bridge.c +++ b/app_pojavlauncher/src/main/jni/egl_bridge.c @@ -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")), diff --git a/app_pojavlauncher/src/main/jni/input_bridge_v3.c b/app_pojavlauncher/src/main/jni/input_bridge_v3.c index d522df52c..04a1a1854 100644 --- a/app_pojavlauncher/src/main/jni/input_bridge_v3.c +++ b/app_pojavlauncher/src/main/jni/input_bridge_v3.c @@ -17,6 +17,7 @@ #include #include #include +#include #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 diff --git a/app_pojavlauncher/src/main/jniLibs/arm64-v8a/libjnidispatch.so b/app_pojavlauncher/src/main/jniLibs/arm64-v8a/libjnidispatch.so index d42762d36..b58816595 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/arm64-v8a/libjnidispatch.so and b/app_pojavlauncher/src/main/jniLibs/arm64-v8a/libjnidispatch.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl.so b/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl.so index ffd734d0e..aa8acb31e 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl.so and b/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_nanovg.so b/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_nanovg.so new file mode 100644 index 000000000..c78fa1565 Binary files /dev/null and b/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_nanovg.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_opengl.so b/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_opengl.so index b8b470592..bad44ab62 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_opengl.so and b/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_opengl.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_stb.so b/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_stb.so index b5e49839f..57ee82465 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_stb.so and b/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_stb.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_tinyfd.so b/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_tinyfd.so new file mode 100644 index 000000000..42af1df5f Binary files /dev/null and b/app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_tinyfd.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/libjnidispatch.so b/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/libjnidispatch.so index c411147d7..1d02dca9d 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/libjnidispatch.so and b/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/libjnidispatch.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl.so b/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl.so index f795db9de..b94f9b931 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl.so and b/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_nanovg.so b/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_nanovg.so new file mode 100644 index 000000000..aea966ada Binary files /dev/null and b/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_nanovg.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_opengl.so b/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_opengl.so index 9312f05e3..44dff1b80 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_opengl.so and b/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_opengl.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_stb.so b/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_stb.so index d7b3c67fc..fd8ce114c 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_stb.so and b/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_stb.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_tinyfd.so b/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_tinyfd.so new file mode 100644 index 000000000..d30a1539a Binary files /dev/null and b/app_pojavlauncher/src/main/jniLibs/armeabi-v7a/liblwjgl_tinyfd.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/x86/libjnidispatch.so b/app_pojavlauncher/src/main/jniLibs/x86/libjnidispatch.so index bb9058cbd..336aad7b9 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/x86/libjnidispatch.so and b/app_pojavlauncher/src/main/jniLibs/x86/libjnidispatch.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl.so b/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl.so index e84adaeac..798847f89 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl.so and b/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_nanovg.so b/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_nanovg.so new file mode 100644 index 000000000..8bf786578 Binary files /dev/null and b/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_nanovg.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_opengl.so b/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_opengl.so index 82c66a026..bf0f05c70 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_opengl.so and b/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_opengl.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_stb.so b/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_stb.so index 3b0c3d68c..7aebedf41 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_stb.so and b/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_stb.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_tinyfd.so b/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_tinyfd.so new file mode 100644 index 000000000..4c9d5eaa2 Binary files /dev/null and b/app_pojavlauncher/src/main/jniLibs/x86/liblwjgl_tinyfd.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/x86_64/libjnidispatch.so b/app_pojavlauncher/src/main/jniLibs/x86_64/libjnidispatch.so index 72ad7b972..4af42fc52 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/x86_64/libjnidispatch.so and b/app_pojavlauncher/src/main/jniLibs/x86_64/libjnidispatch.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl.so b/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl.so index 372fb2900..c9efd0115 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl.so and b/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_nanovg.so b/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_nanovg.so new file mode 100644 index 000000000..283728cdc Binary files /dev/null and b/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_nanovg.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_opengl.so b/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_opengl.so index 3c81abb3e..b863fdf78 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_opengl.so and b/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_opengl.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_stb.so b/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_stb.so index 35e27f248..ae0d6450a 100644 Binary files a/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_stb.so and b/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_stb.so differ diff --git a/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_tinyfd.so b/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_tinyfd.so new file mode 100644 index 000000000..500ae098f Binary files /dev/null and b/app_pojavlauncher/src/main/jniLibs/x86_64/liblwjgl_tinyfd.so differ diff --git a/app_pojavlauncher/src/main/res/drawable/background_control_editor.xml b/app_pojavlauncher/src/main/res/drawable/background_control_editor.xml new file mode 100644 index 000000000..5e350dd5b --- /dev/null +++ b/app_pojavlauncher/src/main/res/drawable/background_control_editor.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app_pojavlauncher/src/main/res/drawable/background_line_selected.xml b/app_pojavlauncher/src/main/res/drawable/background_line_selected.xml index 56fa839b7..f962a8189 100644 --- a/app_pojavlauncher/src/main/res/drawable/background_line_selected.xml +++ b/app_pojavlauncher/src/main/res/drawable/background_line_selected.xml @@ -7,10 +7,6 @@ - - diff --git a/app_pojavlauncher/src/main/res/drawable/background_line_unselected.xml b/app_pojavlauncher/src/main/res/drawable/background_line_unselected.xml index d1cd8faba..1c95d5431 100644 --- a/app_pojavlauncher/src/main/res/drawable/background_line_unselected.xml +++ b/app_pojavlauncher/src/main/res/drawable/background_line_unselected.xml @@ -8,8 +8,8 @@ + android:bottom="@dimen/padding_input_bottom" + android:top="@dimen/padding_input_top" /> + + + + + diff --git a/app_pojavlauncher/src/main/res/drawable/ic_curseforge.png b/app_pojavlauncher/src/main/res/drawable/ic_curseforge.png new file mode 100644 index 000000000..2e19dc710 Binary files /dev/null and b/app_pojavlauncher/src/main/res/drawable/ic_curseforge.png differ diff --git a/app_pojavlauncher/src/main/res/drawable/ic_fabric.webp b/app_pojavlauncher/src/main/res/drawable/ic_fabric.webp new file mode 100644 index 000000000..1e1bb37e1 Binary files /dev/null and b/app_pojavlauncher/src/main/res/drawable/ic_fabric.webp differ diff --git a/app_pojavlauncher/src/main/res/drawable/ic_filter.xml b/app_pojavlauncher/src/main/res/drawable/ic_filter.xml new file mode 100644 index 000000000..b03044595 --- /dev/null +++ b/app_pojavlauncher/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,9 @@ + + + diff --git a/app_pojavlauncher/src/main/res/drawable/ic_modrinth.png b/app_pojavlauncher/src/main/res/drawable/ic_modrinth.png new file mode 100644 index 000000000..0d7b08659 Binary files /dev/null and b/app_pojavlauncher/src/main/res/drawable/ic_modrinth.png differ diff --git a/app_pojavlauncher/src/main/res/drawable/ic_pojav_full.webp b/app_pojavlauncher/src/main/res/drawable/ic_pojav_full.webp new file mode 100644 index 000000000..f67320c77 Binary files /dev/null and b/app_pojavlauncher/src/main/res/drawable/ic_pojav_full.webp differ diff --git a/app_pojavlauncher/src/main/res/drawable/ic_quilt.webp b/app_pojavlauncher/src/main/res/drawable/ic_quilt.webp new file mode 100644 index 000000000..0e9abc098 Binary files /dev/null and b/app_pojavlauncher/src/main/res/drawable/ic_quilt.webp differ diff --git a/app_pojavlauncher/src/main/res/layout/dialog_color_selector.xml b/app_pojavlauncher/src/main/res/layout/dialog_color_selector.xml index ffa57362c..dccb2e998 100644 --- a/app_pojavlauncher/src/main/res/layout/dialog_color_selector.xml +++ b/app_pojavlauncher/src/main/res/layout/dialog_color_selector.xml @@ -5,11 +5,13 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="@dimen/_280sdp" android:layout_height="match_parent" - android:background="@color/background_app" - android:paddingHorizontal="@dimen/_5sdp" - android:paddingVertical="@dimen/_5sdp" + android:background="@drawable/background_control_editor" + android:paddingHorizontal="@dimen/padding_moderate" + android:paddingVertical="@dimen/padding_moderate" android:id="@+id/color_picker_layout" - android:layout_marginVertical="@dimen/_14sdp"> + android:layout_marginVertical="@dimen/padding_heavy" + android:layout_gravity="center_vertical" + > - @@ -58,7 +58,7 @@ + + app:layout_constraintTop_toBottomOf="@+id/editMapping_textView" /> + + + + + + + + + + + - - - + app:layout_constraintBottom_toBottomOf="@+id/mapping_1_textview" + app:layout_constraintEnd_toEndOf="@+id/mapping_1_textview" + app:layout_constraintStart_toEndOf="@id/mapping_1_textview" + app:layout_constraintTop_toTopOf="@+id/mapping_1_textview" /> - - + + + app:layout_constraintTop_toBottomOf="@id/mapping_1_textview" + + tools:text="HELLO" /> + app:layout_constraintBottom_toBottomOf="@+id/mapping_3_textview" + app:layout_constraintEnd_toEndOf="@+id/mapping_3_textview" + app:layout_constraintStart_toEndOf="@id/mapping_3_textview" + app:layout_constraintTop_toBottomOf="@+id/mapping_1_textview" /> - + app:layout_constraintStart_toEndOf="@id/mapping_3_textview" + app:layout_constraintTop_toTopOf="@id/mapping_3_textview" + + tools:text="HELLO" /> @@ -198,17 +267,18 @@ android:id="@+id/editOrientation_textView" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/_2sdp" android:gravity="center" android:paddingEnd="5dp" android:text="@string/customctrl_orientation" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/editMapping_spinner_3" /> + app:layout_constraintTop_toBottomOf="@+id/mapping_3_textview" /> + + + - + app:layout_constraintTop_toBottomOf="@id/checkboxForwardLock" /> @@ -274,6 +354,7 @@ android:id="@+id/editStrokeWidth_textView" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/_5sdp" android:gravity="center" android:paddingEnd="5dp" android:text="@string/customctrl_stroke_width" @@ -285,7 +366,7 @@ + + + + + + + diff --git a/app_pojavlauncher/src/main/res/layout/dialog_mod_filters.xml b/app_pojavlauncher/src/main/res/layout/dialog_mod_filters.xml new file mode 100644 index 000000000..911f02b39 --- /dev/null +++ b/app_pojavlauncher/src/main/res/layout/dialog_mod_filters.xml @@ -0,0 +1,61 @@ + + + + + + + + +