Repository: PojavLauncherTeam/PojavLauncher (language: java)

Issue:
Change the license in README.md
### Is this request related to a bug?

as you see in the title, just change it

### Describe the request.

PojavLauncher is licensed under GNU GPLv3. → LGPL-3.0 License

### Anything else?

_No response_


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 279950573b..fc276e6282 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -56,9 +56,9 @@ jobs:
           branch: buildjre17-21
           name: jre21-pojav
 
-      - uses: gradle/gradle-build-action@v2
+      - uses: gradle/actions/setup-gradle@v4
         with:
-          gradle-version: 7.6.1
+          gradle-version: "8.11"
 
       - name: Build JRE JAR files
         run: |
diff --git a/GPLAY_PRIVACY_POLICY b/GPLAY_PRIVACY_POLICY
index ef9a8a43d1..f55c08fde5 100644
--- a/GPLAY_PRIVACY_POLICY
+++ b/GPLAY_PRIVACY_POLICY
@@ -1,3 +1,3 @@
-1. This app (while idle) does NOT collect any sensitive information, and does NOT use network (exception is the "News" page, it uses network to load the launcher news)
+1. This app (while idle) does NOT collect any sensitive information, and uses network for downloading Minecraft resources.
 2. While running Minecraft, app also does NOT collect any sensitive information about your device. Snooper by Mojang does.
 3. Some sensitive data is stored in crash reports after the game crashes, but it's not being shared to anyone except the current user.
diff --git a/README.md b/README.md
index a874f73836..0acec9fd93 100644
--- a/README.md
+++ b/README.md
@@ -112,7 +112,7 @@ Then, run these commands ~~or build using Android Studio~~.
 - Probably more, that's why we have a bug tracker ;) 
 
 ## License
-- PojavLauncher is licensed under [GNU GPLv3](https://github.com/khanhduytran0/PojavLauncher/blob/master/LICENSE).
+- PojavLauncher is licensed under [GNU LGPLv3](https://github.com/PojavLauncherTeam/PojavLauncher/blob/v3_openjdk/LICENSE).
 
 ## Contributing
 Contributions are welcome! We welcome any type of contribution, not only code. For example, you can help the wiki shape up. You can help the [translation](https://crowdin.com/project/pojavlauncher) too!
diff --git a/app_pojavlauncher/build.gradle b/app_pojavlauncher/build.gradle
index 3654bba93c..034ff33b65 100644
--- a/app_pojavlauncher/build.gradle
+++ b/app_pojavlauncher/build.gradle
@@ -1,5 +1,5 @@
 plugins {
-    id 'com.android.application' version '7.4.2'
+    id 'com.android.application' version '8.7.2'
 }
 
 static def getDate() { return new Date().format('yyyyMMdd') }
@@ -137,6 +137,10 @@ android {
             minifyEnabled true
             shrinkResources true
         }
+        proguardNoDebug {
+            initWith proguard
+            debuggable false
+        }
 
         release {
             // Don't set to true or java.awt will be a.a or something similar.
@@ -182,9 +186,15 @@ android {
 
     buildFeatures {
         prefab true
+        buildConfig true
     }
 
-    buildToolsVersion = '33.0.2'
+    buildToolsVersion = '34.0.0'
+}
+
+afterEvaluate {
+    // Explicit dependencies for which the apk relies on
+    tasks.mergeDebugAssets.dependsOn(":forge_installer:jar", ":arc_dns_injector:jar", ":jre_lwjgl3glfw:jar")
 }
 
 dependencies {
@@ -204,7 +214,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:2.0.3'
-    implementation 'com.github.Mathias-Boulay:virtual-joystick-android:2e7aa25e50'
+    implementation 'com.github.Mathias-Boulay:virtual-joystick-android:1.14'
 
     // implementation 'com.intuit.sdp:sdp-android:1.0.5'
     // implementation 'com.intuit.ssp:ssp-android:1.0.5'
diff --git a/app_pojavlauncher/src/main/AndroidManifest.xml b/app_pojavlauncher/src/main/AndroidManifest.xml
index 98628756a7..ebaf69481c 100644
--- a/app_pojavlauncher/src/main/AndroidManifest.xml
+++ b/app_pojavlauncher/src/main/AndroidManifest.xml
@@ -55,7 +55,8 @@
         </activity>
         <activity
             android:name=".LauncherActivity"
-            android:label="@string/app_short_name" />
+            android:label="@string/app_short_name"
+            android:windowSoftInputMode="adjustResize"/>
         <activity
             android:name=".ImportControlActivity"
             android:configChanges="keyboard|keyboardHidden"
@@ -106,7 +107,7 @@
         <activity
             android:name=".MainActivity"
             android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|keyboard|navigation|uiMode"
-            android:launchMode="standard"
+            android:launchMode="singleTop"
             android:process=":game"
             android:screenOrientation="sensorLandscape" />
 
diff --git a/app_pojavlauncher/src/main/java/com/kdt/CustomSeekbar.java b/app_pojavlauncher/src/main/java/com/kdt/CustomSeekbar.java
index 050276e3e2..5a240ee9f3 100644
--- a/app_pojavlauncher/src/main/java/com/kdt/CustomSeekbar.java
+++ b/app_pojavlauncher/src/main/java/com/kdt/CustomSeekbar.java
@@ -2,9 +2,15 @@
 
 import android.annotation.SuppressLint;
 import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Build;
 import android.util.AttributeSet;
 import android.widget.SeekBar;
 
+import androidx.annotation.Nullable;
+
+import net.kdt.pojavlaunch.R;
+
 /**
  * Seekbar with ability to handle ranges and increments
  */
@@ -14,27 +20,67 @@ public class CustomSeekbar extends SeekBar {
     private int mIncrement = 1;
     private SeekBar.OnSeekBarChangeListener mListener;
 
-    /** When using increments, this flag is used to prevent double calls to the listener */
-    private boolean mInternalChanges = false;
+    private final OnSeekBarChangeListener mInternalListener = new OnSeekBarChangeListener() {
+        /** When using increments, this flag is used to prevent double calls to the listener */
+        private boolean internalChanges = false;
+        /** Store the previous progress to prevent double calls with increments */
+        private int previousProgress = 0;
+        @Override
+        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+            if (internalChanges) return;
+            internalChanges = true;
+
+            progress += mMin;
+            progress = applyIncrement(progress);
+
+            if (progress != previousProgress) {
+                if (mListener != null) {
+                    previousProgress = progress;
+                    mListener.onProgressChanged(seekBar, progress, fromUser);
+                }
+            }
+
+            // Forces the thumb to snap to the increment
+            setProgress(progress);
+            internalChanges = false;
+        }
+
+        @Override
+        public void onStartTrackingTouch(SeekBar seekBar) {
+            if (internalChanges) return;
+
+            if (mListener != null) {
+                mListener.onStartTrackingTouch(seekBar);
+            }
+        }
+
+        @Override
+        public void onStopTrackingTouch(SeekBar seekBar) {
+            if (internalChanges) return;
+            internalChanges = true;
+
+            setProgress(seekBar.getProgress());
+
+            if (mListener != null) {
+                mListener.onStopTrackingTouch(seekBar);
+            }
+            internalChanges = false;
+        }
+    };
 
     public CustomSeekbar(Context context) {
         super(context);
-        setup();
+        setup(null);
     }
 
     public CustomSeekbar(Context context, AttributeSet attrs) {
         super(context, attrs);
-        setup();
+        setup(attrs);
     }
 
     public CustomSeekbar(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        setup();
-    }
-
-    public CustomSeekbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-        setup();
+        setup(attrs);
     }
 
     public void setIncrement(int increment) {
@@ -63,10 +109,15 @@ public synchronized int getProgress() {
 
     @Override
     public synchronized void setMin(int min) {
-        super.setMin(min);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            super.setMin(0);
+        }
         mMin = min;
+        //todo perform something to update the progress ?
     }
 
+
+
     /**
      * Wrapper to allow for a listener to be set around the internal listener
      */
@@ -75,52 +126,27 @@ public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) {
         mListener = l;
     }
 
-    public void setup() {
-        super.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
-            /** Store the previous progress to prevent double calls with increments */
-            private int previousProgress = 0;
-            @Override
-            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-                if (mInternalChanges) return;
-                mInternalChanges = true;
-
-                progress += mMin;
-                progress = applyIncrement(progress);
-
-                if (progress != previousProgress) {
-                    if (mListener != null) {
-                        previousProgress = progress;
-                        mListener.onProgressChanged(seekBar, progress, fromUser);
-                    }
-                }
-
-                // Forces the thumb to snap to the increment
-                setProgress(progress);
-                mInternalChanges = false;
-            }
-
-            @Override
-            public void onStartTrackingTouch(SeekBar seekBar) {
-                if (mInternalChanges) return;
-
-                if (mListener != null) {
-                    mListener.onStartTrackingTouch(seekBar);
-                }
-            }
-
-            @Override
-            public void onStopTrackingTouch(SeekBar seekBar) {
-                if (mInternalChanges) return;
-                mInternalChanges = true;
-
-                setProgress(seekBar.getProgress());
-
-                if (mListener != null) {
-                    mListener.onStopTrackingTouch(seekBar);
-                }
-                mInternalChanges = false;
+    public void setup(@Nullable AttributeSet attrs) {
+        try (TypedArray attributes = getContext().obtainStyledAttributes(attrs, R.styleable.CustomSeekbar)) {
+            setIncrement(attributes.getInt(R.styleable.CustomSeekbar_seekBarIncrement, 1));
+            int min = attributes.getInt(R.styleable.CustomSeekbar_android_min, 0);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                super.setMin(0);
             }
-        });
+            setRange(min, super.getMax());
+        }
+
+        // Due to issues with negative progress when setting up the seekbar
+        // We need to set a random progress to force the refresh of the thumb
+        if(super.getProgress() == 0) {
+            super.setProgress(super.getProgress() + 1);
+            post(() -> {
+                super.setProgress(super.getProgress() - 1);
+                post(() -> super.setOnSeekBarChangeListener(mInternalListener));
+            });
+        } else {
+            super.setOnSeekBarChangeListener(mInternalListener);
+        }
     }
 
     /**
diff --git a/app_pojavlauncher/src/main/java/com/kdt/SideDialogView.java b/app_pojavlauncher/src/main/java/com/kdt/SideDialogView.java
index 5570295105..60784ad4d7 100644
--- a/app_pojavlauncher/src/main/java/com/kdt/SideDialogView.java
+++ b/app_pojavlauncher/src/main/java/com/kdt/SideDialogView.java
@@ -186,6 +186,11 @@ protected final boolean isAtRight() {
      */
     @CallSuper
     public final void disappear(boolean destroy) {
+        if(!mIsInstantiated) {
+            Log.w("SideDialogView", "Layout not inflated");
+            return;
+        }
+
         if (!mDisplaying) {
             if(destroy) {
                 onDisappear();
diff --git a/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcAccountSpinner.java b/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcAccountSpinner.java
index 9280aa2aed..0c576ade2f 100644
--- a/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcAccountSpinner.java
+++ b/app_pojavlauncher/src/main/java/com/kdt/mcgui/mcAccountSpinner.java
@@ -18,12 +18,13 @@
 import android.view.ViewGroup;
 import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
-import android.widget.BaseAdapter;
+import android.widget.ImageView;
 import android.widget.Toast;
 
 import androidx.annotation.Keep;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.widget.AppCompatSpinner;
 import androidx.core.content.res.ResourcesCompat;
 
@@ -188,7 +189,10 @@ protected void onDraw(Canvas canvas) {
     }
 
     public void removeCurrentAccount(){
-        int position = getSelectedItemPosition();
+        removeAccount(getSelectedItemPosition());
+    }
+
+    private void removeAccount(int position) {
         if(position == 0) return;
         File accountFile = new File(Tools.DIR_ACCOUNT_NEW, mAccountList.get(position)+".json");
         if(accountFile.exists()) accountFile.delete();
@@ -321,8 +325,9 @@ private void setImageFromSelectedAccount(){
         BitmapDrawable oldBitmapDrawable = mHeadDrawable;
 
         if(mSelectecAccount != null){
-            ExtendedTextView view = ((ExtendedTextView) getSelectedView());
-            if(view != null){
+            View layout = getSelectedView();
+            if(layout != null){
+                ExtendedTextView view = layout.findViewById(R.id.account_item);
                 Bitmap bitmap = mSelectecAccount.getSkinFace();
                 if(bitmap != null) {
                     mHeadDrawable = new BitmapDrawable(getResources(), bitmap);
@@ -339,8 +344,7 @@ private void setImageFromSelectedAccount(){
         }
     }
 
-
-    private static class AccountAdapter extends ArrayAdapter<String> {
+    private class AccountAdapter extends ArrayAdapter<String> {
 
         private final HashMap<String, Drawable> mImageCache = new HashMap<>();
         public AccountAdapter(@NonNull Context context, int resource, @NonNull String[] objects) {
@@ -349,20 +353,19 @@ public AccountAdapter(@NonNull Context context, int resource, @NonNull String[]
 
         @Override
         public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
-            return getView(position, convertView, parent);
-        }
-
-        @NonNull
-        @Override
-        public View getView(int position, View convertView, @NonNull ViewGroup parent) {
             if(convertView == null){
                 convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_minecraft_account, parent, false);
             }
-            ExtendedTextView textview = (ExtendedTextView) convertView;
+
+            ExtendedTextView textview = convertView.findViewById(R.id.account_item);
+            ImageView deleteButton = convertView.findViewById(R.id.delete_account_button);
             textview.setText(super.getItem(position));
 
             // Handle the "Add account section"
-            if(position == 0) textview.setCompoundDrawables(ResourcesCompat.getDrawable(parent.getResources(), R.drawable.ic_add, null), null, null, null);
+            if(position == 0) {
+                textview.setCompoundDrawables(ResourcesCompat.getDrawable(parent.getResources(), R.drawable.ic_add, null), null, null, null);
+                deleteButton.setVisibility(View.GONE);
+            }
             else {
                 String username = super.getItem(position);
                 Drawable accountHead = mImageCache.get(username);
@@ -371,9 +374,37 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
                     mImageCache.put(username, accountHead);
                 }
                 textview.setCompoundDrawables(accountHead, null, null, null);
+
+                deleteButton.setVisibility(View.VISIBLE);
+                deleteButton.setOnClickListener(v -> {
+                    showDeleteDialog(getContext(), position);
+                });
             }
             return convertView;
         }
+
+
+
+        @NonNull
+        @Override
+        public View getView(int position, View convertView, @NonNull ViewGroup parent) {
+            View view = getDropDownView(position, convertView, parent);
+            view.findViewById(R.id.delete_account_button).setVisibility(View.GONE);
+            return view;
+        }
+
+        private void showDeleteDialog(Context context, int position) {
+            new AlertDialog.Builder(context)
+                    .setMessage(R.string.warning_remove_account)
+                    .setPositiveButton(android.R.string.cancel, null)
+                    .setNeutralButton(R.string.global_delete, (dialog, which) -> {
+                        onDetachedFromWindow();
+                        removeAccount(position);
+                    })
+                    .show();
+        }
     }
 
+
+
 }
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/CriticalNativeTest.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/CriticalNativeTest.java
index 434b0e44b7..9f72ce919f 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/CriticalNativeTest.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/CriticalNativeTest.java
@@ -1,7 +1,10 @@
 package net.kdt.pojavlaunch;
 
+import androidx.annotation.Keep;
+
 import dalvik.annotation.optimization.CriticalNative;
 
+@Keep
 public class CriticalNativeTest {
     @CriticalNative
     public static native void testCriticalNative(int arg0, int arg1);
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 73bd5977d9..44a873b47c 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java
@@ -59,7 +59,7 @@ public class LauncherActivity extends BaseActivity {
 
     private mcAccountSpinner mAccountSpinner;
     private FragmentContainerView mFragmentView;
-    private ImageButton mSettingsButton, mDeleteAccountButton;
+    private ImageButton mSettingsButton;
     private ProgressLayout mProgressLayout;
     private ProgressServiceKeeper mProgressServiceKeeper;
     private ModloaderInstallTracker mInstallTracker;
@@ -101,13 +101,6 @@ public void onFragmentResumed(@NonNull FragmentManager fm, @NonNull Fragment f)
         }
     };
 
-    /* Listener for account deletion */
-    private final View.OnClickListener mAccountDeleteButtonListener = v -> new AlertDialog.Builder(this)
-            .setMessage(R.string.warning_remove_account)
-            .setPositiveButton(android.R.string.cancel, null)
-            .setNeutralButton(R.string.global_delete, (dialog, which) -> mAccountSpinner.removeCurrentAccount())
-            .show();
-
     private final ExtraListener<Boolean> mLaunchGameListener = (key, value) -> {
         if(mProgressLayout.hasProcesses()){
             Toast.makeText(this, R.string.tasks_ongoing, Toast.LENGTH_LONG).show();
@@ -156,7 +149,12 @@ public void onFragmentResumed(@NonNull FragmentManager fm, @NonNull Fragment f)
 
     @Override
     protected boolean shouldIgnoreNotch() {
-        return getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT || super.shouldIgnoreNotch();
+        return getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
+    }
+
+    @Override
+    public boolean setFullscreen() {
+        return false;
     }
 
     @Override
@@ -195,7 +193,6 @@ protected void onCreate(Bundle savedInstanceState) {
         ProgressKeeper.addTaskCountListener((mProgressServiceKeeper = new ProgressServiceKeeper(this)));
 
         mSettingsButton.setOnClickListener(mSettingButtonListener);
-        mDeleteAccountButton.setOnClickListener(mAccountDeleteButtonListener);
         ProgressKeeper.addTaskCountListener(mProgressLayout);
         ExtraCore.addExtraListener(ExtraConstants.BACK_PREFERENCE, mBackPreferenceListener);
         ExtraCore.addExtraListener(ExtraConstants.SELECT_AUTH_METHOD, mSelectAuthMethod);
@@ -227,11 +224,6 @@ protected void onPause() {
         mInstallTracker.detach();
     }
 
-    @Override
-    public boolean setFullscreen() {
-        return false;
-    }
-
     @Override
     protected void onStart() {
         super.onStart();
@@ -343,7 +335,6 @@ public void askForNotificationPermission(Runnable onSuccessRunnable) {
     private void bindViews(){
         mFragmentView = findViewById(R.id.container_fragment);
         mSettingsButton = findViewById(R.id.setting_button);
-        mDeleteAccountButton = findViewById(R.id.delete_account_button);
         mAccountSpinner = findViewById(R.id.account_spinner);
         mProgressLayout = findViewById(R.id.progress_layout);
     }
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Logger.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Logger.java
index 168f83d145..bcd5fb4776 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Logger.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Logger.java
@@ -15,6 +15,7 @@ public class Logger {
     public static native void begin(String logFilePath);
 
     /** Small listener for anything listening to the log */
+    @Keep
     public interface eventLogListener {
         void onEventLogged(String text);
     }
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 50eb96e747..bfabd8f368 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java
@@ -36,6 +36,7 @@
 import android.widget.ListView;
 import android.widget.Toast;
 
+import androidx.annotation.Keep;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.core.content.ContextCompat;
@@ -242,8 +243,8 @@ private void loadControls() {
     @Override
     public void onAttachedToWindow() {
         // Post to get the correct display dimensions after layout.
+        LauncherPreferences.computeNotchSize(this);
         mControlLayout.post(()->{
-            LauncherPreferences.computeNotchSize(this);
             Tools.getDisplayMetrics(this);
             loadControls();
         });
@@ -442,6 +443,7 @@ public static void switchKeyboardState() {
         if(touchCharInput != null) touchCharInput.switchKeyboardState();
     }
 
+    @Keep
     public static void openLink(String link) {
         Context ctx = touchpad.getContext(); // no more better way to obtain a context statically
         ((Activity)ctx).runOnUiThread(() -> {
@@ -461,6 +463,7 @@ public static void openLink(String link) {
             }
         });
     }
+
     @SuppressWarnings("unused") //TODO: actually use it
     public static void openPath(String path) {
         Context ctx = touchpad.getContext(); // no more better way to obtain a context statically
@@ -473,6 +476,7 @@ public static void openPath(String path) {
         });
     }
 
+    @Keep
     public static void querySystemClipboard() {
         Tools.runOnUiThread(()->{
             ClipData clipData = GLOBAL_CLIPBOARD.getPrimaryClip();
@@ -491,6 +495,7 @@ public static void querySystemClipboard() {
         });
     }
 
+    @Keep
     public static void putClipboardData(String data, String mimeType) {
         Tools.runOnUiThread(()-> {
             ClipData clipData = null;
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 9bc091e470..316dc08220 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java
@@ -27,15 +27,13 @@
 import android.os.Environment;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.Process;
 import android.provider.DocumentsContract;
 import android.provider.OpenableColumns;
 import android.util.ArrayMap;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.View;
-import android.view.Window;
-import android.view.WindowInsets;
-import android.view.WindowInsetsController;
 import android.view.WindowManager;
 import android.widget.EditText;
 import android.widget.TextView;
@@ -43,10 +41,13 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.core.app.NotificationManagerCompat;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.core.view.WindowInsetsControllerCompat;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
 
@@ -512,70 +513,38 @@ public static DisplayMetrics getDisplayMetrics(Activity activity) {
         return displayMetrics;
     }
 
-    @SuppressWarnings("deprecation")
-    private static void setFullscreenLegacy(Activity activity, boolean fullscreen) {
-        final View decorView = activity.getWindow().getDecorView();
-        View.OnSystemUiVisibilityChangeListener visibilityChangeListener = visibility -> {
-            boolean multiWindowMode = SDK_INT >= 24 && activity.isInMultiWindowMode();
-            // When in multi-window mode, asking for fullscreen makes no sense (cause the launcher runs in a window)
-            // So, ignore the fullscreen setting when activity is in multi window mode
-            if(fullscreen && !multiWindowMode){
-                if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
-                    decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
-                            | View.SYSTEM_UI_FLAG_FULLSCREEN
-                            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
-                            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
-                            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
-                            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
-                }
-            }else{
-                decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
-            }
-
-        };
-        decorView.setOnSystemUiVisibilityChangeListener(visibilityChangeListener);
-        visibilityChangeListener.onSystemUiVisibilityChange(decorView.getSystemUiVisibility()); //call it once since the UI state may not change after the call, so the activity wont become fullscreen
-    }
-
-    @RequiresApi(Build.VERSION_CODES.R)
-    private static void setFullscreenSdk30(Activity activity, boolean fullscreen) {
-        final Window window = activity.getWindow();
-        final View decorView = window.getDecorView();
-        final int insetControllerFlags = WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars();
-        final View.OnApplyWindowInsetsListener windowInsetsListener = (view, windowInsets) ->{
-            WindowInsetsController windowInsetsController = decorView.getWindowInsetsController();
-            if(windowInsetsController == null) return windowInsets;
-            boolean multiWindowMode = activity.isInMultiWindowMode();
-            // Emulate the behaviour of the legacy function using the new flags
-            if(fullscreen && !multiWindowMode) {
-                windowInsetsController.hide(insetControllerFlags);
-                windowInsetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
-                window.setDecorFitsSystemWindows(false);
-            }else {
-                windowInsetsController.show(insetControllerFlags);
-                // Both of the constants below have the exact same numerical value, but
-                // for some reason the one that works below Android S was removed
-                // from the acceptable constants for setSystemBarsBehaviour
-                if (SDK_INT >= Build.VERSION_CODES.S) {
-                    windowInsetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_DEFAULT);
-                }else {
-                    // noinspection WrongConstant
-                    windowInsetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE);
-                }
-                window.setDecorFitsSystemWindows(true);
-            }
-            return windowInsets;
-        };
-        decorView.setOnApplyWindowInsetsListener(windowInsetsListener);
-        windowInsetsListener.onApplyWindowInsets(decorView, null);
-    }
-
     public static void setFullscreen(Activity activity, boolean fullscreen) {
-        if (SDK_INT >= Build.VERSION_CODES.R) {
-            setFullscreenSdk30(activity, fullscreen);
-        }else {
-            setFullscreenLegacy(activity, fullscreen);
+        WindowInsetsControllerCompat windowInsetsController =
+                WindowCompat.getInsetsController(activity.getWindow(), activity.getWindow().getDecorView());
+        if (windowInsetsController == null) {
+            Log.w(APP_NAME, "WindowInsetsController is null, cannot set fullscreen");
+            return;
         }
+
+        // Configure the behavior of the hidden system bars.
+        windowInsetsController.setSystemBarsBehavior(
+                WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+        );
+
+        ViewCompat.setOnApplyWindowInsetsListener(
+                activity.getWindow().getDecorView(),
+                (view, windowInsets) -> {
+                    boolean fullscreenImpl = fullscreen;
+                    if (SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode())
+                        fullscreenImpl = false;
+
+                    if (fullscreenImpl) {
+                        windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
+                    } else {
+                        windowInsetsController.show(WindowInsetsCompat.Type.systemBars());
+                    }
+
+                    if(SDK_INT >= Build.VERSION_CODES.R)
+                        activity.getWindow().setDecorFitsSystemWindows(!fullscreenImpl);
+
+                    return ViewCompat.onApplyWindowInsets(view, windowInsets);
+                });
+
     }
 
     public static DisplayMetrics currentDisplayMetrics;
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 52798daeb0..82b041f2ab 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
@@ -7,6 +7,7 @@
 import androidx.annotation.Keep;
 
 import net.kdt.pojavlaunch.Tools;
+import net.kdt.pojavlaunch.customcontrols.buttons.ControlInterface;
 import net.kdt.pojavlaunch.prefs.LauncherPreferences;
 import net.kdt.pojavlaunch.utils.JSONUtils;
 import net.objecthunter.exp4j.ExpressionBuilder;
@@ -240,7 +241,7 @@ private static void buildConversionMap() {
         keyValueMap.put("height", "DUMMY_HEIGHT");
         keyValueMap.put("screen_width", "DUMMY_DATA");
         keyValueMap.put("screen_height", "DUMMY_DATA");
-        keyValueMap.put("margin", Integer.toString((int) Tools.dpToPx(2)));
+        keyValueMap.put("margin", Integer.toString((int) ControlInterface.getMarginDistance()));
         keyValueMap.put("preferred_scale", "DUMMY_DATA");
 
         conversionMap = new WeakReference<>(keyValueMap);
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 3983b38491..a73f23eede 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
@@ -57,13 +57,13 @@ public CustomControls(Context ctx) {
 		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 V3
-		version = 7;
+		version = 8;
 	}
 
 	
 	public void save(String path) throws IOException {
-		//Current version is the V3.1 so the version as to be marked as 7 !
-		version = 7;
+		//Current version is the V3.2 so the version as to be marked as 8 !
+		version = 8;
 
 		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 25352897bc..81c949fd31 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
@@ -24,27 +24,56 @@ public static CustomControls loadAndConvertIfNecessary(String jsonPath) throws I
                 CustomControls layout = LayoutConverter.convertV1Layout(layoutJobj);
                 layout.save(jsonPath);
                 return layout;
-            } 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") <= 5) {
-                return LayoutConverter.convertV3_4Layout(layoutJobj);
-            } else if (layoutJobj.getInt("version") == 6 || layoutJobj.getInt("version") == 7) {
-                return Tools.GLOBAL_GSON.fromJson(jsonLayoutData, CustomControls.class);
             } else {
-                return null;
+                int version = layoutJobj.getInt("version");
+                if (version == 2) {
+                    CustomControls layout = LayoutConverter.convertV2Layout(layoutJobj);
+                    layout.save(jsonPath);
+                    return layout;
+                }
+                if (version == 3 || version == 4 || version == 5) {
+                    return LayoutConverter.convertV3_4Layout(layoutJobj);
+                }
+                if (version == 6 || version == 7) {
+                    return convertV6_7Layout(layoutJobj);
+                }
+                else if (version == 8) {
+                    return Tools.GLOBAL_GSON.fromJson(jsonLayoutData, CustomControls.class);
+                }
             }
+            return null;
         } catch (JSONException e) {
             throw new JsonSyntaxException("Failed to load", e);
         }
     }
 
 
+    /**
+     * Normalize the layout to v8 from v6/7. An issue from the joystick height and position has to be fixed.
+     * @param oldLayoutJson The old layout
+     * @return The new layout with the fixed joystick height
+     */
+    public static CustomControls convertV6_7Layout(JSONObject oldLayoutJson) {
+        CustomControls layout = Tools.GLOBAL_GSON.fromJson(oldLayoutJson.toString(), CustomControls.class);
+        for (ControlJoystickData data : layout.mJoystickDataList) {
+            if (data.getHeight() > data.getWidth()) {
+                // Make the size square, adjust the dynamic position related to height
+                float ratio = data.getHeight() / data.getWidth();
+
+                data.dynamicX = data.dynamicX.replace("${height}", "(" + ratio + " * ${height})");
+                data.dynamicY = data.dynamicY.replace("${height}", "(" + ratio + " * ${height})") +  " + (" + (ratio-1) + " * ${height})";
+
+                data.setHeight(data.getWidth());
+            }
+        }
+        layout.version = 8;
+        return layout;
+    }
+
     /**
      * 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) {
+    private static CustomControls convertV3_4Layout(JSONObject oldLayoutJson) {
         CustomControls layout = Tools.GLOBAL_GSON.fromJson(oldLayoutJson.toString(), CustomControls.class);
         convertStrokeWidth(layout);
         layout.version = 6;
@@ -52,7 +81,7 @@ public static CustomControls convertV3_4Layout(JSONObject oldLayoutJson) {
     }
 
 
-    public static CustomControls convertV2Layout(JSONObject oldLayoutJson) throws JSONException {
+    private 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());
@@ -95,7 +124,7 @@ public static CustomControls convertV2Layout(JSONObject oldLayoutJson) throws JS
         return layout;
     }
 
-    public static CustomControls convertV1Layout(JSONObject oldLayoutJson) throws JSONException {
+    private static CustomControls convertV1Layout(JSONObject oldLayoutJson) throws JSONException {
         CustomControls empty = new CustomControls();
         JSONArray layoutMainArray = oldLayoutJson.getJSONArray("mControlDataList");
         for (int i = 0; i < layoutMainArray.length(); i++) {
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 2c31604615..1f7bbadf14 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
@@ -61,26 +61,27 @@ private void switchButtonVisibility(){
     private void alignButtons(){
         if(buttons == null) return;
         if(drawerData.orientation == ControlDrawerData.Orientation.FREE) return;
+        int margin = (int) ControlInterface.getMarginDistance();
 
         for(int i = 0; i < buttons.size(); ++i){
             switch (drawerData.orientation){
                 case RIGHT:
-                    buttons.get(i).setDynamicX(generateDynamicX(getX() + (drawerData.properties.getWidth() + Tools.dpToPx(2))*(i+1) ));
+                    buttons.get(i).setDynamicX(generateDynamicX(getX() + (drawerData.properties.getWidth() + margin)*(i+1) ));
                     buttons.get(i).setDynamicY(generateDynamicY(getY()));
                     break;
 
                 case LEFT:
-                    buttons.get(i).setDynamicX(generateDynamicX(getX() - (drawerData.properties.getWidth() + Tools.dpToPx(2))*(i+1)));
+                    buttons.get(i).setDynamicX(generateDynamicX(getX() - (drawerData.properties.getWidth() + margin)*(i+1)));
                     buttons.get(i).setDynamicY(generateDynamicY(getY()));
                     break;
 
                 case UP:
-                    buttons.get(i).setDynamicY(generateDynamicY(getY() - (drawerData.properties.getHeight() + Tools.dpToPx(2))*(i+1)));
+                    buttons.get(i).setDynamicY(generateDynamicY(getY() - (drawerData.properties.getHeight() + margin)*(i+1)));
                     buttons.get(i).setDynamicX(generateDynamicX(getX()));
                     break;
 
                 case DOWN:
-                    buttons.get(i).setDynamicY(generateDynamicY(getY() + (drawerData.properties.getHeight() + Tools.dpToPx(2))*(i+1)));
+                    buttons.get(i).setDynamicY(generateDynamicY(getY() + (drawerData.properties.getHeight() + margin)*(i+1)));
                     buttons.get(i).setDynamicX(generateDynamicX(getX()));
                     break;
             }
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 569ddfefc9..6ae683a460 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
@@ -30,7 +30,6 @@
  * sending keys has to be implemented by sub classes.
  */
 public interface ControlInterface extends View.OnLongClickListener, GrabListener {
-
     View getControlView();
 
     ControlData getProperties();
@@ -214,7 +213,7 @@ default float computeCornerRadius(float radiusInPercent) {
      */
     @SuppressWarnings("BooleanMethodIsAlwaysInverted")
     default boolean canSnap(ControlInterface button) {
-        float MIN_DISTANCE = Tools.dpToPx(8);
+        float MIN_DISTANCE = getSnapDistance();
 
         if (button == this) return false;
         return !(net.kdt.pojavlaunch.utils.MathUtils.dist(
@@ -237,7 +236,7 @@ default boolean canSnap(ControlInterface button) {
      * @param y Coordinate on the y axis
      */
     default void snapAndAlign(float x, float y) {
-        float MIN_DISTANCE = Tools.dpToPx(8);
+        final float MIN_DISTANCE = getSnapDistance();
         String dynamicX = generateDynamicX(x);
         String dynamicY = generateDynamicY(y);
 
@@ -404,4 +403,12 @@ default boolean onLongClick(View v) {
 
         return true;
     }
+
+    static float getSnapDistance() {
+        return Tools.dpToPx(6);
+    }
+
+    static float getMarginDistance() {
+        return Tools.dpToPx(2);
+    }
 }
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/Gamepad.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/Gamepad.java
index 4d3a7b135a..49fccca082 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/Gamepad.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/Gamepad.java
@@ -43,6 +43,7 @@
 import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_WEST;
 import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.isJoystickEvent;
 import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_DEADZONE_SCALE;
+import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_SCALE_FACTOR;
 import static net.kdt.pojavlaunch.utils.MCOptionUtils.getMcScale;
 import static org.lwjgl.glfw.CallbackBridge.sendKeyPress;
 import static org.lwjgl.glfw.CallbackBridge.sendMouseButton;
@@ -52,9 +53,6 @@
 
 public class Gamepad implements GrabListener, GamepadHandler {
 
-    /* Resolution scaler option, allow downsizing a window */
-    private final float mScaleFactor = LauncherPreferences.DEFAULT_PREF.getInt("resolutionRatio",100)/100f;
-
     /* Sensitivity, adjusted according to screen size */
     private final double mSensitivityFactor = (1.4 * (1080f/ currentDisplayMetrics.heightPixels));
 
@@ -116,7 +114,7 @@ public void doFrame(long frameTimeNanos) {
         mPointerImageView.setImageDrawable(ResourcesCompat.getDrawable(ctx.getResources(), R.drawable.ic_gamepad_pointer, ctx.getTheme()));
         mPointerImageView.getDrawable().setFilterBitmap(false);
 
-        int size = (int) ((22 * getMcScale()) / mScaleFactor);
+        int size = (int) ((22 * getMcScale()) / PREF_SCALE_FACTOR);
         mPointerImageView.setLayoutParams(new FrameLayout.LayoutParams(size, size));
 
         mMapProvider = mapProvider;
@@ -155,7 +153,7 @@ public void updateJoysticks(){
 
     public void notifyGUISizeChange(int newSize){
         //Change the pointer size to match UI
-        int size = (int) ((22 * newSize) / mScaleFactor);
+        int size = (int) ((22 * newSize) / PREF_SCALE_FACTOR);
         mPointerImageView.post(() -> mPointerImageView.setLayoutParams(new FrameLayout.LayoutParams(size, size)));
 
     }
@@ -228,7 +226,7 @@ private void tick(long frameTimeNanos){
             if(!isGrabbing){
                 CallbackBridge.mouseX = MathUtils.clamp(CallbackBridge.mouseX, 0, CallbackBridge.windowWidth);
                 CallbackBridge.mouseY = MathUtils.clamp(CallbackBridge.mouseY, 0, CallbackBridge.windowHeight);
-                placePointerView((int) (CallbackBridge.mouseX / mScaleFactor), (int) (CallbackBridge.mouseY/ mScaleFactor));
+                placePointerView((int) (CallbackBridge.mouseX / PREF_SCALE_FACTOR), (int) (CallbackBridge.mouseY/ PREF_SCALE_FACTOR));
             }
 
             //Send the mouse to the game
@@ -340,7 +338,7 @@ public void onGrabState(boolean isGrabbing) {
         placePointerView(CallbackBridge.physicalWidth/2, CallbackBridge.physicalHeight/2);
         mPointerImageView.setVisibility(View.VISIBLE);
         // Sensitivity in menu is MC and HARDWARE resolution dependent
-        mMouseSensitivity = 19 * mScaleFactor / mSensitivityFactor;
+        mMouseSensitivity = 19 * PREF_SCALE_FACTOR / mSensitivityFactor;
     }
 
     @Override
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/DrawerPullButton.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/DrawerPullButton.java
index fd6fc4714f..fb90307527 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/DrawerPullButton.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/DrawerPullButton.java
@@ -16,23 +16,22 @@ public class DrawerPullButton extends View {
     public DrawerPullButton(Context context) {super(context); init();}
     public DrawerPullButton(Context context, @Nullable AttributeSet attrs) {super(context, attrs); init();}
 
-    private final Paint mPaint = new Paint();
+    private final Paint mBackgroundPaint = new Paint();
     private VectorDrawableCompat mDrawable;
 
     private void init(){
         mDrawable = VectorDrawableCompat.create(getContext().getResources(), R.drawable.ic_sharp_settings_24, null);
         setAlpha(0.33f);
+        mBackgroundPaint.setColor(Color.BLACK);
     }
 
     @Override
     protected void onDraw(Canvas canvas) {
-        mPaint.setColor(Color.BLACK);
-        canvas.drawArc(0,-getHeight(),getWidth(), getHeight(), 0, 180, true, mPaint);
+        canvas.drawArc(getPaddingLeft(),-getHeight() + getPaddingBottom(),getWidth() - getPaddingRight(), getHeight() - getPaddingBottom(), 0, 180, true, mBackgroundPaint);
 
-        mPaint.setColor(Color.WHITE);
-        mDrawable.setBounds(0, 0, getHeight(), getHeight());
+        mDrawable.setBounds(getPaddingLeft()/2, getPaddingTop()/2, getHeight() - getPaddingRight()/2, getHeight() - getPaddingBottom()/2);
         canvas.save();
-        canvas.translate((getWidth()-getHeight())/2f, 0);
+        canvas.translate((getWidth()-getHeight())/2f, -getPaddingBottom()/2f);
         mDrawable.draw(canvas);
         canvas.restore();
     }
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlSideDialog.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlSideDialog.java
index 14bf4dde4f..47b9b68d9f 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlSideDialog.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlSideDialog.java
@@ -332,6 +332,7 @@ private void bindLayout() {
     /**
      * A long function linking all the displayed data on the popup and,
      * the currently edited mCurrentlyEditedButton
+     * @noinspection SuspiciousNameCombination
      */
     private void setupRealTimeListeners() {
         mNameEditText.addTextChangedListener((SimpleTextWatcher) s -> {
@@ -349,6 +350,10 @@ private void setupRealTimeListeners() {
             float width = safeParseFloat(s.toString());
             if (width >= 0) {
                 mCurrentlyEditedButton.getProperties().setWidth(width);
+                if (mCurrentlyEditedButton.getProperties() instanceof ControlJoystickData) {
+                    // Joysticks are square
+                     mCurrentlyEditedButton.getProperties().setHeight(width);
+                }
                 mCurrentlyEditedButton.updateProperties();
             }
         });
@@ -359,6 +364,10 @@ private void setupRealTimeListeners() {
             float height = safeParseFloat(s.toString());
             if (height >= 0) {
                 mCurrentlyEditedButton.getProperties().setHeight(height);
+                if (mCurrentlyEditedButton.getProperties() instanceof ControlJoystickData) {
+                    // Joysticks are square
+                    mCurrentlyEditedButton.getProperties().setWidth(height);
+                }
                 mCurrentlyEditedButton.updateProperties();
             }
         });
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/HotbarView.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/HotbarView.java
index 522b0fe5b8..b969fc83a1 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/HotbarView.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/HotbarView.java
@@ -11,6 +11,7 @@
 
 import androidx.annotation.Nullable;
 
+import net.kdt.pojavlaunch.GrabListener;
 import net.kdt.pojavlaunch.LwjglGlfwKeycode;
 import net.kdt.pojavlaunch.prefs.LauncherPreferences;
 import net.kdt.pojavlaunch.utils.MCOptionUtils;
@@ -26,8 +27,16 @@ public class HotbarView extends View implements MCOptionUtils.MCOptionListener,
             LwjglGlfwKeycode.GLFW_KEY_4, LwjglGlfwKeycode.GLFW_KEY_5,   LwjglGlfwKeycode.GLFW_KEY_6,
             LwjglGlfwKeycode.GLFW_KEY_7, LwjglGlfwKeycode.GLFW_KEY_8, LwjglGlfwKeycode.GLFW_KEY_9};
     private final DropGesture mDropGesture = new DropGesture(new Handler(Looper.getMainLooper()));
+    private final GrabListener mGrabListener = new GrabListener() {
+        @Override
+        public void onGrabState(boolean isGrabbing) {
+            mLastIndex = -1;
+            mDropGesture.cancel();
+        }
+    };
+
     private int mWidth;
-    private int mLastIndex;
+    private int mLastIndex = -1;
     private int mGuiScale;
 
     public HotbarView(Context context) {
@@ -66,6 +75,13 @@ protected void onAttachedToWindow() {
         }
         mGuiScale = MCOptionUtils.getMcScale();
         repositionView();
+        CallbackBridge.addGrabListener(mGrabListener);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        CallbackBridge.removeGrabListener(mGrabListener);
     }
 
     private void repositionView() {
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/InGameEventProcessor.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/InGameEventProcessor.java
index f4e5fe7639..bc05007002 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/InGameEventProcessor.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/InGameEventProcessor.java
@@ -33,8 +33,12 @@ public boolean processTouchEvent(MotionEvent motionEvent) {
             case MotionEvent.ACTION_MOVE:
                 mTracker.trackEvent(motionEvent);
                 float[] motionVector = mTracker.getMotionVector();
-                CallbackBridge.mouseX += (float) (motionVector[0] * mSensitivity);
-                CallbackBridge.mouseY += (float) (motionVector[1] * mSensitivity);
+                float deltaX = (float) (motionVector[0] * mSensitivity);
+                float deltaY = (float) (motionVector[1] * mSensitivity);
+                mLeftClickGesture.setMotion(deltaX, deltaY);
+                mRightClickGesture.setMotion(deltaX, deltaY);
+                CallbackBridge.mouseX += deltaX;
+                CallbackBridge.mouseY += deltaY;
                 CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY);
                 if(LauncherPreferences.PREF_DISABLE_GESTURES) break;
                 checkGestures();
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/LeftClickGesture.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/LeftClickGesture.java
index d422ed6838..259117ff51 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/LeftClickGesture.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/LeftClickGesture.java
@@ -13,23 +13,28 @@
 
 public class LeftClickGesture extends ValidatorGesture {
     public static final int FINGER_STILL_THRESHOLD = (int) Tools.dpToPx(9);
-    private float mGestureStartX, mGestureStartY;
+    private float mGestureStartX, mGestureStartY, mGestureEndX, mGestureEndY;
     private boolean mMouseActivated;
 
     public LeftClickGesture(Handler handler) {
-        super(handler, LauncherPreferences.PREF_LONGPRESS_TRIGGER);
+        super(handler);
     }
 
     public final void inputEvent() {
         if(submit()) {
-            mGestureStartX = CallbackBridge.mouseX;
-            mGestureStartY = CallbackBridge.mouseY;
+            mGestureStartX = mGestureEndX = CallbackBridge.mouseX;
+            mGestureStartY = mGestureEndY = CallbackBridge.mouseY;
         }
     }
 
+    @Override
+    protected int getGestureDelay() {
+        return LauncherPreferences.PREF_LONGPRESS_TRIGGER;
+    }
+
     @Override
     public boolean checkAndTrigger() {
-        boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY, FINGER_STILL_THRESHOLD);
+        boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY, mGestureEndX, mGestureEndY, FINGER_STILL_THRESHOLD);
         // If the finger is still, fire the gesture.
         if(fingerStill) {
             sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, true);
@@ -47,6 +52,11 @@ public void onGestureCancelled(boolean isSwitching) {
         }
     }
 
+    public void setMotion(float deltaX, float deltaY) {
+        mGestureEndX += deltaX;
+        mGestureEndY += deltaY;
+    }
+
     /**
      * Check if the finger is still when compared to mouseX/mouseY in CallbackBridge.
      * @param startX the starting X of the gesture
@@ -61,4 +71,13 @@ public static boolean isFingerStill(float startX, float startY, float threshold)
                 startY
         ) <= threshold;
     }
+
+    public static boolean isFingerStill(float startX, float startY, float endX, float endY, float threshold) {
+        return MathUtils.dist(
+                endX,
+                endY,
+                startX,
+                startY
+        ) <= threshold;
+    }
 }
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/RightClickGesture.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/RightClickGesture.java
index 75ccbba6c7..84c5efd183 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/RightClickGesture.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/RightClickGesture.java
@@ -6,24 +6,35 @@
 
 import org.lwjgl.glfw.CallbackBridge;
 
-public class RightClickGesture extends ValidatorGesture{
+public class RightClickGesture extends ValidatorGesture {
     private boolean mGestureEnabled = true;
     private boolean mGestureValid = true;
-    private float mGestureStartX, mGestureStartY;
+    private float mGestureStartX, mGestureStartY, mGestureEndX, mGestureEndY;
     public RightClickGesture(Handler mHandler) {
-        super(mHandler, 150);
+        super(mHandler);
     }
 
     public final void inputEvent() {
         if(!mGestureEnabled) return;
         if(submit()) {
-            mGestureStartX = CallbackBridge.mouseX;
-            mGestureStartY = CallbackBridge.mouseY;
+            mGestureStartX = mGestureEndX = CallbackBridge.mouseX;
+            mGestureStartY = mGestureEndY = CallbackBridge.mouseY;
             mGestureEnabled = false;
             mGestureValid = true;
         }
     }
 
+    public void setMotion(float deltaX, float deltaY) {
+        System.out.println("set motion called");
+        mGestureEndX += deltaX;
+        mGestureEndY += deltaY;
+    }
+
+    @Override
+    protected int getGestureDelay() {
+        return 150;
+    }
+
     @Override
     public boolean checkAndTrigger() {
         // If the validate() method was called, it means that the user held on for too long. The cancellation should be ignored.
@@ -38,7 +49,8 @@ public boolean checkAndTrigger() {
     public void onGestureCancelled(boolean isSwitching) {
         mGestureEnabled = true;
         if(!mGestureValid || isSwitching) return;
-        boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY, LeftClickGesture.FINGER_STILL_THRESHOLD);
+        boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY, mGestureEndX, mGestureEndY, LeftClickGesture.FINGER_STILL_THRESHOLD);
+        System.out.println("Right click: " + fingerStill);
         if(!fingerStill) return;
         CallbackBridge.sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, true);
         CallbackBridge.sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, false);
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/ValidatorGesture.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/ValidatorGesture.java
index 20956d5675..a61e45c67c 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/ValidatorGesture.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/mouse/ValidatorGesture.java
@@ -9,16 +9,13 @@
 public abstract class ValidatorGesture implements Runnable{
     private final Handler mHandler;
     private boolean mGestureActive;
-    private final int mRequiredDuration;
 
     /**
      * @param mHandler the Handler that will be used for calling back the checkAndTrigger() method.
      *                 This Handler should run on the same thread as the callee of submit()/cancel()
-     * @param mRequiredDuration the duration after which the class will call checkAndTrigger().
      */
-    public ValidatorGesture(Handler mHandler, int mRequiredDuration) {
+    public ValidatorGesture(Handler mHandler) {
         this.mHandler = mHandler;
-        this.mRequiredDuration = mRequiredDuration;
     }
 
     /**
@@ -28,7 +25,7 @@ public ValidatorGesture(Handler mHandler, int mRequiredDuration) {
      */
     public final boolean submit() {
         if(mGestureActive) return false;
-        mHandler.postDelayed(this, mRequiredDuration);
+        mHandler.postDelayed(this, getGestureDelay());
         mGestureActive = true;
         return true;
     }
@@ -55,7 +52,13 @@ public final void run() {
     }
 
     /**
-     * This method will be called after mRequiredDuration milliseconds, if the gesture was not cancelled.
+     * This method will be called during gesture submission to determine the gesture check duration.
+     * @return the required gesture check duration in milliseconds
+     */
+    protected abstract int getGestureDelay();
+
+    /**
+     * This method will be called after getGestureDelay() milliseconds, if the gesture was not cancelled.
      * @return false if you want to mark this gesture as "inactive"
      *         true otherwise
      */
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java
index 088747b110..501b0147a7 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java
@@ -107,7 +107,28 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
             Tools.removeCurrentFragment(requireActivity());
         });
 
-        mGameDirButton.setOnClickListener(v -> {
+
+        View.OnClickListener gameDirListener = getGameDirListener();
+        mGameDirButton.setOnClickListener(gameDirListener);
+        mDefaultPath.setOnClickListener(gameDirListener);
+
+        View.OnClickListener controlSelectListener = getControlSelectListener();
+        mControlSelectButton.setOnClickListener(controlSelectListener);
+        mDefaultControl.setOnClickListener(controlSelectListener);
+
+        // Setup the expendable list behavior
+        View.OnClickListener versionSelectListener = getVersionSelectListener();
+        mVersionSelectButton.setOnClickListener(versionSelectListener);
+        mDefaultVersion.setOnClickListener(versionSelectListener);
+
+        // Set up the icon change click listener
+        mProfileIcon.setOnClickListener(v -> CropperUtils.startCropper(mCropperLauncher));
+
+        loadValues(LauncherPreferences.DEFAULT_PREF.getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, ""), view.getContext());
+    }
+
+    private View.OnClickListener getGameDirListener() {
+        return v -> {
             Bundle bundle = new Bundle(2);
             bundle.putBoolean(FileSelectorFragment.BUNDLE_SELECT_FOLDER, true);
             bundle.putString(FileSelectorFragment.BUNDLE_ROOT_PATH, Tools.DIR_GAME_HOME);
@@ -116,9 +137,11 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
 
             Tools.swapFragment(requireActivity(),
                     FileSelectorFragment.class, FileSelectorFragment.TAG, bundle);
-        });
+        };
+    }
 
-        mControlSelectButton.setOnClickListener(v -> {
+    private View.OnClickListener getControlSelectListener() {
+        return v -> {
             Bundle bundle = new Bundle(3);
             bundle.putBoolean(FileSelectorFragment.BUNDLE_SELECT_FOLDER, false);
             bundle.putString(FileSelectorFragment.BUNDLE_ROOT_PATH, Tools.CTRLMAP_PATH);
@@ -126,20 +149,14 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
 
             Tools.swapFragment(requireActivity(),
                     FileSelectorFragment.class, FileSelectorFragment.TAG, bundle);
-        });
+        };
+    }
 
-        // Setup the expendable list behavior
-        mVersionSelectButton.setOnClickListener(v -> VersionSelectorDialog.open(v.getContext(), false, (id, snapshot)->{
+    private View.OnClickListener getVersionSelectListener() {
+        return v -> VersionSelectorDialog.open(v.getContext(), false, (id, snapshot)-> {
             mTempProfile.lastVersionId = id;
             mDefaultVersion.setText(id);
-        }));
-
-        // Set up the icon change click listener
-        mProfileIcon.setOnClickListener(v -> CropperUtils.startCropper(mCropperLauncher));
-
-
-
-        loadValues(LauncherPreferences.DEFAULT_PREF.getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, ""), view.getContext());
+        });
     }
 
 
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java
index d53b0617ea..f24159a328 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java
@@ -43,7 +43,7 @@ public static void downloadFileMirrored(int downloadClass, String urlInput, File
             return;
         }catch (FileNotFoundException e) {
             Log.w("DownloadMirror", "Cannot find the file on the mirror", e);
-            Log.i("DownloadMirror", "Failling back to default source");
+            Log.i("DownloadMirror", "Falling back to default source");
         }
         DownloadUtils.downloadFileMonitored(urlInput, outputFile, buffer, monitor);
     }
@@ -63,11 +63,30 @@ public static void downloadFileMirrored(int downloadClass, String urlInput, File
             return;
         }catch (FileNotFoundException e) {
             Log.w("DownloadMirror", "Cannot find the file on the mirror", e);
-            Log.i("DownloadMirror", "Failling back to default source");
+            Log.i("DownloadMirror", "Falling back to default source");
         }
         DownloadUtils.downloadFile(urlInput, outputFile);
     }
 
+    /**
+     * Get the content length of a file on the current mirror. If the file is missing on the mirror,
+     * or the mirror does not give out the length, request the length from the original source
+     * @param downloadClass Class of the download. Can either be DOWNLOAD_CLASS_LIBRARIES,
+     *                      DOWNLOAD_CLASS_METADATA or DOWNLOAD_CLASS_ASSETS
+     * @param urlInput The original (Mojang) URL for the download
+     * @return the length of the file denoted by the URL in bytes, or -1 if not available
+     */
+    public static long getContentLengthMirrored(int downloadClass, String urlInput) throws IOException {
+        long length = DownloadUtils.getContentLength(getMirrorMapping(downloadClass, urlInput));
+        if(length < 1) {
+            Log.w("DownloadMirror", "Unable to get content length from mirror");
+            Log.i("DownloadMirror", "Falling back to default source");
+            return DownloadUtils.getContentLength(urlInput);
+        }else {
+            return length;
+        }
+    }
+
     /**
      * Check if the current download source is a mirror and not an official source.
      * @return true if the source is a mirror, false otherwise
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/plugins/FFmpegPlugin.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/plugins/FFmpegPlugin.java
index bd1b308124..514a176555 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/plugins/FFmpegPlugin.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/plugins/FFmpegPlugin.java
@@ -5,15 +5,21 @@
 import android.content.pm.PackageManager;
 import android.util.Log;
 
+import java.io.File;
+
 public class FFmpegPlugin {
     public static boolean isAvailable = false;
     public static String libraryPath;
+    public static String executablePath;
     public static void discover(Context context) {
         PackageManager manager = context.getPackageManager();
         try {
             PackageInfo ffmpegPluginInfo = manager.getPackageInfo("net.kdt.pojavlaunch.ffmpeg", PackageManager.GET_SHARED_LIBRARY_FILES);
             libraryPath = ffmpegPluginInfo.applicationInfo.nativeLibraryDir;
-            isAvailable = true;
+            File ffmpegExecutable = new File(libraryPath, "libffmpeg.so");
+            executablePath = ffmpegExecutable.getAbsolutePath();
+            // Older plugin versions still have the old executable location
+            isAvailable = ffmpegExecutable.exists();
         }catch (Exception e) {
             Log.i("FFmpegPlugin", "Failed to discover plugin", e);
         }
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/CustomSeekBarPreference.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/CustomSeekBarPreference.java
index 0978de9541..5bb1bd131f 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/CustomSeekBarPreference.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/CustomSeekBarPreference.java
@@ -28,10 +28,10 @@ public class CustomSeekBarPreference extends SeekBarPreference {
     @SuppressLint("PrivateResource")
     public CustomSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        TypedArray a = context.obtainStyledAttributes(
-                attrs, R.styleable.SeekBarPreference, defStyleAttr, defStyleRes);
-        mMin = a.getInt(R.styleable.SeekBarPreference_min, 0);
-        a.recycle();
+        try (TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.SeekBarPreference, defStyleAttr, defStyleRes)) {
+            mMin = a.getInt(R.styleable.SeekBarPreference_min, 0);
+        }
     }
 
     public CustomSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java
index 5e73275034..b5480596f7 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java
@@ -10,17 +10,15 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.Build;
+import android.util.DisplayMetrics;
 import android.util.Log;
 
-import androidx.core.view.DisplayCutoutCompat;
-import androidx.core.view.WindowCompat;
-import androidx.core.view.WindowInsetsCompat;
-import androidx.core.view.WindowInsetsControllerCompat;
-
 import net.kdt.pojavlaunch.*;
 import net.kdt.pojavlaunch.multirt.MultiRTUtils;
 import net.kdt.pojavlaunch.utils.JREUtils;
 
+import java.io.IOException;
+
 public class LauncherPreferences {
     public static final String PREF_KEY_CURRENT_PROFILE = "currentProfile";
     public static final String PREF_KEY_SKIP_NOTIFICATION_CHECK = "skipNotificationPermissionCheck";
@@ -28,11 +26,6 @@ public class LauncherPreferences {
     public static SharedPreferences DEFAULT_PREF;
     public static String PREF_RENDERER = "opengles2";
 
-	public static boolean PREF_VERTYPE_RELEASE = true;
-	public static boolean PREF_VERTYPE_SNAPSHOT = false;
-	public static boolean PREF_VERTYPE_OLDALPHA = false;
-	public static boolean PREF_VERTYPE_OLDBETA = false;
-	public static boolean PREF_HIDE_SIDEBAR = false;
 	public static boolean PREF_IGNORE_NOTCH = false;
 	public static int PREF_NOTCH_SIZE = 0;
 	public static float PREF_BUTTONSIZE = 100f;
@@ -54,14 +47,14 @@ public class LauncherPreferences {
     public static boolean PREF_USE_ALTERNATE_SURFACE = true;
     public static boolean PREF_JAVA_SANDBOX = true;
     public static float PREF_SCALE_FACTOR = 1f;
+
     public static boolean PREF_ENABLE_GYRO = false;
     public static float PREF_GYRO_SENSITIVITY = 1f;
     public static int PREF_GYRO_SAMPLE_RATE = 16;
     public static boolean PREF_GYRO_SMOOTHING = true;
-
     public static boolean PREF_GYRO_INVERT_X = false;
-
     public static boolean PREF_GYRO_INVERT_Y = false;
+
     public static boolean PREF_FORCE_VSYNC = false;
 
     public static boolean PREF_BUTTON_ALL_CAPS = true;
@@ -79,17 +72,13 @@ public class LauncherPreferences {
     public static void loadPreferences(Context ctx) {
         //Required for the data folder.
         Tools.initContextConstants(ctx);
+        boolean isDevicePowerful = isDevicePowerful(ctx);
 
         PREF_RENDERER = DEFAULT_PREF.getString("renderer", "opengles2");
         PREF_BUTTONSIZE = DEFAULT_PREF.getInt("buttonscale", 100);
         PREF_MOUSESCALE = DEFAULT_PREF.getInt("mousescale", 100)/100f;
-		PREF_MOUSESPEED = ((float)DEFAULT_PREF.getInt("mousespeed",100))/100f;
-		PREF_HIDE_SIDEBAR = DEFAULT_PREF.getBoolean("hideSidebar", false);
-		PREF_IGNORE_NOTCH = DEFAULT_PREF.getBoolean("ignoreNotch", false);
-		PREF_VERTYPE_RELEASE = DEFAULT_PREF.getBoolean("vertype_release", true);
-		PREF_VERTYPE_SNAPSHOT = DEFAULT_PREF.getBoolean("vertype_snapshot", false);
-		PREF_VERTYPE_OLDALPHA = DEFAULT_PREF.getBoolean("vertype_oldalpha", false);
-		PREF_VERTYPE_OLDBETA = DEFAULT_PREF.getBoolean("vertype_oldbeta", false);
+        PREF_MOUSESPEED = ((float)DEFAULT_PREF.getInt("mousespeed",100))/100f;
+        PREF_IGNORE_NOTCH = DEFAULT_PREF.getBoolean("ignoreNotch", false);
 		PREF_LONGPRESS_TRIGGER = DEFAULT_PREF.getInt("timeLongPressTrigger", 300);
 		PREF_DEFAULTCTRL_PATH = DEFAULT_PREF.getString("defaultCtrl", Tools.CTRLDEF_FILE);
         PREF_FORCE_ENGLISH = DEFAULT_PREF.getBoolean("force_english", false);
@@ -98,19 +87,19 @@ public static void loadPreferences(Context ctx) {
         PREF_DISABLE_SWAP_HAND = DEFAULT_PREF.getBoolean("disableDoubleTap", false);
         PREF_RAM_ALLOCATION = DEFAULT_PREF.getInt("allocation", findBestRAMAllocation(ctx));
         PREF_CUSTOM_JAVA_ARGS = DEFAULT_PREF.getString("javaArgs", "");
-        PREF_SUSTAINED_PERFORMANCE = DEFAULT_PREF.getBoolean("sustainedPerformance", false);
+        PREF_SUSTAINED_PERFORMANCE = DEFAULT_PREF.getBoolean("sustainedPerformance", isDevicePowerful);
         PREF_VIRTUAL_MOUSE_START = DEFAULT_PREF.getBoolean("mouse_start", false);
         PREF_ARC_CAPES = DEFAULT_PREF.getBoolean("arc_capes",false);
-        PREF_USE_ALTERNATE_SURFACE = DEFAULT_PREF.getBoolean("alternate_surface", false);
+        PREF_USE_ALTERNATE_SURFACE = DEFAULT_PREF.getBoolean("alternate_surface", isDevicePowerful);
         PREF_JAVA_SANDBOX = DEFAULT_PREF.getBoolean("java_sandbox", true);
-        PREF_SCALE_FACTOR = DEFAULT_PREF.getInt("resolutionRatio", 100)/100f;
+        PREF_SCALE_FACTOR = DEFAULT_PREF.getInt("resolutionRatio", findBestResolution(ctx, isDevicePowerful))/100f;
         PREF_ENABLE_GYRO = DEFAULT_PREF.getBoolean("enableGyro", false);
         PREF_GYRO_SENSITIVITY = ((float)DEFAULT_PREF.getInt("gyroSensitivity", 100))/100f;
         PREF_GYRO_SAMPLE_RATE = DEFAULT_PREF.getInt("gyroSampleRate", 16);
         PREF_GYRO_SMOOTHING = DEFAULT_PREF.getBoolean("gyroSmoothing", true);
         PREF_GYRO_INVERT_X = DEFAULT_PREF.getBoolean("gyroInvertX", false);
         PREF_GYRO_INVERT_Y = DEFAULT_PREF.getBoolean("gyroInvertY", false);
-        PREF_FORCE_VSYNC = DEFAULT_PREF.getBoolean("force_vsync", false);
+        PREF_FORCE_VSYNC = DEFAULT_PREF.getBoolean("force_vsync", isDevicePowerful);
         PREF_BUTTON_ALL_CAPS = DEFAULT_PREF.getBoolean("buttonAllCaps", true);
         PREF_DUMP_SHADERS = DEFAULT_PREF.getBoolean("dump_shaders", false);
         PREF_DEADZONE_SCALE = ((float) DEFAULT_PREF.getInt("gamepad_deadzone_scale", 100))/100f;
@@ -132,7 +121,7 @@ public static void loadPreferences(Context ctx) {
         if(DEFAULT_PREF.contains("defaultRuntime")) {
             PREF_DEFAULT_RUNTIME = DEFAULT_PREF.getString("defaultRuntime","");
         }else{
-            if(MultiRTUtils.getRuntimes().size() < 1) {
+            if(MultiRTUtils.getRuntimes().isEmpty()) {
                 PREF_DEFAULT_RUNTIME = "";
                 return;
             }
@@ -164,6 +153,46 @@ private static int findBestRAMAllocation(Context ctx){
         return 2048; //Default RAM allocation for 64 bits
     }
 
+    /// Find a correct resolution for the device
+    ///
+    /// Some devices are shipped with a ridiculously high resolution, which can cause performance issues
+    /// This function will try to find a resolution that is good enough for the device
+    private static int findBestResolution(Context context, boolean isDevicePowerful) {
+        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+        int minSide = Math.min(metrics.widthPixels, metrics.heightPixels);
+        int targetSide = isDevicePowerful ? 1080 : 720;
+        if (minSide <= targetSide) return 100; // No need to scale down
+
+        float ratio = (100f * targetSide / minSide);
+        // The value must match the seekbar values
+        int increment = context.getResources().getInteger(R.integer.resolution_seekbar_increment);
+        return (int) (Math.ceil(ratio / increment) * increment);
+    }
+
+    /// Check if the device is considered powerful.
+    /// Powerful devices will have some energy saving tweaks enabled by default
+    private static boolean isDevicePowerful(Context context) {
+        if (SDK_INT < Build.VERSION_CODES.Q) return false;
+        if (Tools.getTotalDeviceMemory(context) <= 4096) return false;
+        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+        if (Math.min(metrics.widthPixels, metrics.heightPixels) < 1080) return false;
+        if (Runtime.getRuntime().availableProcessors() <= 4) return false;
+        if (hasAllCoreSameFreq()) return false;
+        return true;
+    }
+
+    private static boolean hasAllCoreSameFreq() {
+        int coreCount = Runtime.getRuntime().availableProcessors();
+        try {
+            String freq0 = Tools.read("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq");
+            String freqX = Tools.read("/sys/devices/system/cpu/cpu" + (coreCount - 1) + "/cpufreq/cpuinfo_max_freq");
+            if(freq0.equals(freqX)) return true;
+        } catch (IOException e) {
+            Log.e("LauncherPreferences", "Failed to read CPU frequencies", e);
+        }
+        return false;
+    }
+
     /** Compute the notch size to avoid being out of bounds */
     public static void computeNotchSize(Activity activity) {
         if (Build.VERSION.SDK_INT < P) return;
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/QuickSettingSideDialog.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/QuickSettingSideDialog.java
index 47ebb7a809..5458dbf572 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/QuickSettingSideDialog.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/QuickSettingSideDialog.java
@@ -122,8 +122,6 @@ private void setupListeners() {
             mEditor.putBoolean("disableGestures", isChecked);
         });
 
-        mGyroSensitivityBar.setRange(25, 300);
-        mGyroSensitivityBar.setIncrement(5);
         mGyroSensitivityBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> {
             PREF_GYRO_SENSITIVITY = progress / 100f;
             mEditor.putInt("gyroSensitivity", progress);
@@ -132,8 +130,6 @@ private void setupListeners() {
         mGyroSensitivityBar.setProgress((int) (mOriginalGyroSensitivity * 100f));
         setSeekTextPercent(mGyroSensitivityText, mGyroSensitivityBar.getProgress());
 
-        mMouseSpeedBar.setRange(25, 300);
-        mMouseSpeedBar.setIncrement(5);
         mMouseSpeedBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> {
             PREF_MOUSESPEED = progress / 100f;
             mEditor.putInt("mousespeed", progress);
@@ -142,8 +138,6 @@ private void setupListeners() {
         mMouseSpeedBar.setProgress((int) (mOriginalMouseSpeed * 100f));
         setSeekTextPercent(mMouseSpeedText, mMouseSpeedBar.getProgress());
 
-        mGestureDelayBar.setRange(100, 1000);
-        mGestureDelayBar.setIncrement(10);
         mGestureDelayBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> {
             PREF_LONGPRESS_TRIGGER = progress;
             mEditor.putInt("timeLongPressTrigger", progress);
@@ -152,8 +146,6 @@ private void setupListeners() {
         mGestureDelayBar.setProgress(mOriginalGestureDelay);
         setSeekTextMillisecond(mGestureDelayText, mGestureDelayBar.getProgress());
 
-        mResolutionBar.setRange(25, 100);
-        mResolutionBar.setIncrement(5);
         mResolutionBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> {
             PREF_SCALE_FACTOR = progress/100f;
             mEditor.putInt("resolutionRatio", progress);
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceControlFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceControlFragment.java
index 88ac51ffd8..11f34e5dc2 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceControlFragment.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceControlFragment.java
@@ -32,31 +32,26 @@ public void onCreatePreferences(Bundle b, String str) {
 
         CustomSeekBarPreference seek2 = requirePreference("timeLongPressTrigger",
                 CustomSeekBarPreference.class);
-        seek2.setRange(100, 1000);
         seek2.setValue(longPressTrigger);
         seek2.setSuffix(" ms");
 
         CustomSeekBarPreference seek3 = requirePreference("buttonscale",
                 CustomSeekBarPreference.class);
-        seek3.setRange(80, 250);
         seek3.setValue(prefButtonSize);
         seek3.setSuffix(" %");
 
         CustomSeekBarPreference seek4 = requirePreference("mousescale",
                 CustomSeekBarPreference.class);
-        seek4.setRange(25, 300);
         seek4.setValue(mouseScale);
         seek4.setSuffix(" %");
 
         CustomSeekBarPreference seek6 = requirePreference("mousespeed",
                 CustomSeekBarPreference.class);
-        seek6.setRange(25, 300);
         seek6.setValue((int)(mouseSpeed *100f));
         seek6.setSuffix(" %");
 
         CustomSeekBarPreference deadzoneSeek = requirePreference("gamepad_deadzone_scale",
                 CustomSeekBarPreference.class);
-        deadzoneSeek.setRange(50, 200);
         deadzoneSeek.setValue((int) (joystickDeadzone * 100f));
         deadzoneSeek.setSuffix(" %");
 
@@ -71,13 +66,11 @@ public void onCreatePreferences(Bundle b, String str) {
 
         CustomSeekBarPreference gyroSensitivitySeek = requirePreference("gyroSensitivity",
                 CustomSeekBarPreference.class);
-        gyroSensitivitySeek.setRange(25, 300);
         gyroSensitivitySeek.setValue((int) (gyroSpeed*100f));
         gyroSensitivitySeek.setSuffix(" %");
 
         CustomSeekBarPreference gyroSampleRateSeek = requirePreference("gyroSampleRate",
                 CustomSeekBarPreference.class);
-        gyroSampleRateSeek.setRange(5, 50);
         gyroSampleRateSeek.setValue(gyroSampleRate);
         gyroSampleRateSeek.setSuffix(" ms");
         computeVisibility();
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceJavaFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceJavaFragment.java
index 13c5b26a92..245bf84855 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceJavaFragment.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceJavaFragment.java
@@ -29,19 +29,18 @@ public void onCreatePreferences(Bundle b, String str) {
         // Triggers a write for some reason
         addPreferencesFromResource(R.xml.pref_java);
 
-        CustomSeekBarPreference seek7 = requirePreference("allocation",
+        CustomSeekBarPreference memorySeekbar = requirePreference("allocation",
                 CustomSeekBarPreference.class);
 
         int maxRAM;
-        int deviceRam = getTotalDeviceMemory(seek7.getContext());
+        int deviceRam = getTotalDeviceMemory(memorySeekbar.getContext());
 
         if(is32BitsDevice() || deviceRam < 2048) maxRAM = Math.min(1024, deviceRam);
         else maxRAM = deviceRam - (deviceRam < 3064 ? 800 : 1024); //To have a minimum for the device to breathe
 
-        seek7.setMin(256);
-        seek7.setMax(maxRAM);
-        seek7.setValue(ramAllocation);
-        seek7.setSuffix(" MB");
+        memorySeekbar.setMax(maxRAM);
+        memorySeekbar.setValue(ramAllocation);
+        memorySeekbar.setSuffix(" MB");
 
         EditTextPreference editJVMArgs = findPreference("javaArgs");
         if (editJVMArgs != null) {
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceVideoFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceVideoFragment.java
index 6876f2aa45..14f412e513 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceVideoFragment.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceVideoFragment.java
@@ -1,7 +1,5 @@
 package net.kdt.pojavlaunch.prefs.screens;
 
-import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_NOTCH_SIZE;
-
 import android.content.SharedPreferences;
 import android.os.Build;
 import android.os.Bundle;
@@ -22,24 +20,30 @@ public class LauncherPreferenceVideoFragment extends LauncherPreferenceFragment
     @Override
     public void onCreatePreferences(Bundle b, String str) {
         addPreferencesFromResource(R.xml.pref_video);
+        int resolution = (int) (LauncherPreferences.PREF_SCALE_FACTOR * 100);
 
         //Disable notch checking behavior on android 8.1 and below.
-        requirePreference("ignoreNotch").setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && PREF_NOTCH_SIZE > 0);
+        requirePreference("ignoreNotch").setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && LauncherPreferences.PREF_NOTCH_SIZE > 0);
 
-        CustomSeekBarPreference seek5 = requirePreference("resolutionRatio",
+        CustomSeekBarPreference resolutionSeekbar = requirePreference("resolutionRatio",
                 CustomSeekBarPreference.class);
-        seek5.setMin(25);
-        seek5.setSuffix(" %");
+        resolutionSeekbar.setSuffix(" %");
 
         // #724 bug fix
-        if (seek5.getValue() < 25) {
-            seek5.setValue(100);
+        if (resolution < 25) {
+            resolutionSeekbar.setValue(100);
+        } else {
+            resolutionSeekbar.setValue(resolution);
         }
 
         // Sustained performance is only available since Nougat
         SwitchPreference sustainedPerfSwitch = requirePreference("sustainedPerformance",
                 SwitchPreference.class);
         sustainedPerfSwitch.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N);
+        sustainedPerfSwitch.setChecked(LauncherPreferences.PREF_SUSTAINED_PERFORMANCE);
+
+        requirePreference("alternate_surface", SwitchPreferenceCompat.class).setChecked(LauncherPreferences.PREF_USE_ALTERNATE_SURFACE);
+        requirePreference("force_vsync", SwitchPreferenceCompat.class).setChecked(LauncherPreferences.PREF_FORCE_VSYNC);
 
         ListPreference rendererListPreference = requirePreference("renderer",
                 ListPreference.class);
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 e582652d6f..70a01ae017 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
@@ -95,21 +95,21 @@ public void setViewProfile(View v, String nm, boolean displaySelection) {
         Drawable cachedIcon = ProfileIconCache.fetchIcon(v.getResources(), nm, minecraftProfile.icon);
         extendedTextView.setCompoundDrawablesRelative(cachedIcon, null, extendedTextView.getCompoundsDrawables()[2], null);
 
-        if(Tools.isValidString(minecraftProfile.name))
-            extendedTextView.setText(minecraftProfile.name);
-        else
-            extendedTextView.setText(R.string.unnamed);
-
-        if(minecraftProfile.lastVersionId != null){
-            if(minecraftProfile.lastVersionId.equalsIgnoreCase("latest-release")){
-                extendedTextView.setText( String.format("%s - %s", extendedTextView.getText(), v.getContext().getText(R.string.profiles_latest_release)));
-            } else if(minecraftProfile.lastVersionId.equalsIgnoreCase("latest-snapshot")){
-                extendedTextView.setText( String.format("%s - %s", extendedTextView.getText(), v.getContext().getText(R.string.profiles_latest_snapshot)));
-            } else {
-                extendedTextView.setText( String.format("%s - %s", extendedTextView.getText(), minecraftProfile.lastVersionId));
-            }
-
-        } else extendedTextView.setText(extendedTextView.getText());
+        // Historically, the profile name "New" was hardcoded as the default profile name
+        // We consider "New" the same as putting no name at all
+        String profileName = (Tools.isValidString(minecraftProfile.name) && !"New".equalsIgnoreCase(minecraftProfile.name)) ? minecraftProfile.name : null;
+        String versionName = minecraftProfile.lastVersionId;
+
+        if (MinecraftProfile.LATEST_RELEASE.equalsIgnoreCase(versionName))
+            versionName = v.getContext().getString(R.string.profiles_latest_release);
+        else if (MinecraftProfile.LATEST_SNAPSHOT.equalsIgnoreCase(versionName))
+            versionName = v.getContext().getString(R.string.profiles_latest_snapshot);
+
+        if (versionName == null && profileName != null)
+            extendedTextView.setText(profileName);
+        else if (versionName != null && profileName == null)
+            extendedTextView.setText(versionName);
+        else extendedTextView.setText(String.format("%s - %s", profileName, versionName));
 
         // Set selected background if needed
         if(displaySelection){
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/FolderProvider.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/FolderProvider.java
index 2ce9dc9013..e2286e6307 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/FolderProvider.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/FolderProvider.java
@@ -1,10 +1,12 @@
 package net.kdt.pojavlaunch.scoped;
 
 import android.annotation.TargetApi;
+import android.content.ContentResolver;
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.graphics.Point;
+import android.net.Uri;
 import android.os.CancellationSignal;
 import android.os.ParcelFileDescriptor;
 import android.provider.DocumentsContract;
@@ -16,6 +18,7 @@
 
 import androidx.annotation.Nullable;
 
+import net.kdt.pojavlaunch.BuildConfig;
 import net.kdt.pojavlaunch.R;
 import net.kdt.pojavlaunch.Tools;
 
@@ -46,6 +49,9 @@ public class FolderProvider extends DocumentsProvider {
 
     private static final File BASE_DIR = new File(Tools.DIR_GAME_HOME);
 
+    private ContentResolver mContentResolver;
+
+    private String mStorageProviderAuthortiy;
 
     // The default columns to return information about a root if no specific
     // columns are requested in a query.
@@ -76,10 +82,15 @@ public Cursor queryRoots(String[] projection) {
         final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
         final String applicationName = getContext().getString(R.string.app_short_name);
 
+        String summary = BuildConfig.VERSION_NAME;
+        if (BuildConfig.DEBUG) {
+            summary = "(" + getContext().getString(R.string.generic_debug) + ") " + summary;
+        }
+
         final MatrixCursor.RowBuilder row = result.newRow();
         row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
         row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
-        row.add(Root.COLUMN_SUMMARY, null);
+        row.add(Root.COLUMN_SUMMARY, summary);
         row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
         row.add(Root.COLUMN_TITLE, applicationName);
         row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
@@ -91,6 +102,8 @@ public Cursor queryRoots(String[] projection) {
     @Override
     public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
         final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
+        // Future-proofing in case if we implement realtime file watching
+        result.setNotificationUri(mContentResolver, createUriForDocId(documentId));
         includeFile(result, documentId, null);
         return result;
     }
@@ -104,6 +117,8 @@ public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
         for (File file : children) {
             includeFile(result, null, file);
         }
+        // Set the notification URI as that's what the "Files" app will be listening to in case of file deletion
+        result.setNotificationUri(mContentResolver, createUriForDocId(parentDocumentId));
         return result;
     }
 
@@ -123,6 +138,8 @@ public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHi
 
     @Override
     public boolean onCreate() {
+        mContentResolver = getContext().getContentResolver();
+        mStorageProviderAuthortiy = getContext().getString(R.string.storageProviderAuthorities);
         return true;
     }
 
@@ -146,6 +163,8 @@ public String createDocument(String parentDocumentId, String mimeType, String di
         } catch (IOException e) {
             throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
         }
+        // Notify the file manager that the parent directory has changed
+        notifyChange(createUriForDocId(parentDocumentId));
         return newFile.getPath();
     }
 
@@ -190,6 +209,8 @@ public void deleteDocument(String documentId) throws FileNotFoundException {
                 throw new FileNotFoundException("Failed to delete document with id " + documentId);
             }
         }
+        // Notify the file manager that the parent directory has changed
+        notifyChange(createUriForFile(file.getParentFile()));
     }
 
     @Override
@@ -332,4 +353,16 @@ public DocumentsContract.Path findDocumentPath(@Nullable String parentDocumentId
         Log.i("FolderProvider", pathIds.toString());
         return new DocumentsContract.Path(getDocIdForFile(source), pathIds);
     }
+
+    private Uri createUriForDocId(String documentId) throws FileNotFoundException {
+        return createUriForFile(getFileForDocId(documentId));
+    }
+
+    private Uri createUriForFile(File file) {
+        return DocumentsContract.buildDocumentUri(mStorageProviderAuthortiy, file.getAbsolutePath());
+    }
+
+    private void notifyChange(Uri uri) {
+        mContentResolver.notifyChange(uri, null);
+    }
 }
\ No newline at end of file
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 43ab1c0879..8782113b89 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
@@ -13,6 +13,7 @@
 import androidx.annotation.Nullable;
 import androidx.core.app.NotificationCompat;
 
+import net.kdt.pojavlaunch.MainActivity;
 import net.kdt.pojavlaunch.R;
 import net.kdt.pojavlaunch.Tools;
 import net.kdt.pojavlaunch.utils.NotificationUtils;
@@ -39,9 +40,14 @@ public int onStartCommand(Intent intent, int flags, int startId) {
         killIntent.putExtra("kill", true);
         PendingIntent pendingKillIntent = PendingIntent.getService(this, NotificationUtils.PENDINGINTENT_CODE_KILL_GAME_SERVICE
                 , killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
+        PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
+                new Intent(this, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
+                 PendingIntent.FLAG_IMMUTABLE);
+
         NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "channel_id")
                 .setContentTitle(getString(R.string.lazy_service_default_title))
                 .setContentText(getString(R.string.notification_game_runs))
+                .setContentIntent(contentIntent)
                 .addAction(android.R.drawable.ic_menu_close_clear_cancel,  getString(R.string.notification_terminate), pendingKillIntent)
                 .setSmallIcon(R.drawable.notif_icon)
                 .setNotificationSilent();
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 d197b4142f..b4d7012b8e 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
@@ -3,13 +3,14 @@
 import net.kdt.pojavlaunch.JMinecraftVersionList;
 import net.kdt.pojavlaunch.extra.ExtraConstants;
 import net.kdt.pojavlaunch.extra.ExtraCore;
+import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
 
 public class AsyncMinecraftDownloader {
     public static String normalizeVersionId(String versionString) {
         JMinecraftVersionList versionList = (JMinecraftVersionList) ExtraCore.getValue(ExtraConstants.RELEASE_TABLE);
         if(versionList == null || versionList.versions == null) return versionString;
-        if("latest-release".equals(versionString)) versionString = versionList.latest.get("release");
-        if("latest-snapshot".equals(versionString)) versionString = versionList.latest.get("snapshot");
+        if(MinecraftProfile.LATEST_RELEASE.equals(versionString)) versionString = versionList.latest.get("release");
+        if(MinecraftProfile.LATEST_SNAPSHOT.equals(versionString)) versionString = versionList.latest.get("snapshot");
         return versionString;
     }
 
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloader.java
index 5abaa57fdb..95074cc804 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloader.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloader.java
@@ -37,14 +37,18 @@
 import java.util.concurrent.atomic.AtomicReference;
 
 public class MinecraftDownloader {
+    private static final double ONE_MEGABYTE = (1024d * 1024d);
     public static final String MINECRAFT_RES = "https://resources.download.minecraft.net/";
     private AtomicReference<Exception> mDownloaderThreadException;
     private ArrayList<DownloaderTask> mScheduledDownloadTasks;
-    private AtomicLong mDownloadFileCounter;
-    private AtomicLong mDownloadSizeCounter;
-    private long mDownloadFileCount;
+    private AtomicLong mProcessedFileCounter;
+    private AtomicLong mProcessedSizeCounter; // Total bytes of processed files (passed SHA1 or downloaded)
+    private AtomicLong mInternetUsageCounter; // How many bytes downloaded over Internet
+    private long mTotalFileCount;
+    private long mTotalSize;
     private File mSourceJarFile; // The source client JAR picked during the inheritance process
     private File mTargetJarFile; // The destination client JAR to which the source will be copied to.
+    private boolean mUseFileCounter; // Whether a file counter or a size counter should be used for progress
 
     private static final ThreadLocal<byte[]> sThreadLocalDownloadBuffer = new ThreadLocal<>();
 
@@ -80,12 +84,15 @@ private void downloadGame(Activity activity, JMinecraftVersionList.Version verIn
         // Put up a dummy progress line, for the activity to start the service and do all the other necessary
         // work to keep the launcher alive. We will replace this line when we will start downloading stuff.
         ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.newdl_starting);
+        SpeedCalculator speedCalculator = new SpeedCalculator();
 
         mTargetJarFile = createGameJarPath(versionName);
         mScheduledDownloadTasks = new ArrayList<>();
-        mDownloadFileCounter = new AtomicLong(0);
-        mDownloadSizeCounter = new AtomicLong(0);
+        mProcessedFileCounter = new AtomicLong(0);
+        mProcessedSizeCounter = new AtomicLong(0);
+        mInternetUsageCounter = new AtomicLong(0);
         mDownloaderThreadException = new AtomicReference<>(null);
+        mUseFileCounter = false;
 
         if(!downloadAndProcessMetadata(activity, verInfo, versionName)) {
             throw new RuntimeException(activity.getString(R.string.exception_failed_to_unpack_jre17));
@@ -104,11 +111,9 @@ private void downloadGame(Activity activity, JMinecraftVersionList.Version verIn
         try {
             while (mDownloaderThreadException.get() == null &&
                     !downloaderPool.awaitTermination(33, TimeUnit.MILLISECONDS)) {
-                long dlFileCounter = mDownloadFileCounter.get();
-                int progress = (int)((dlFileCounter * 100L) / mDownloadFileCount);
-                ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, progress,
-                        R.string.newdl_downloading_game_files, dlFileCounter,
-                        mDownloadFileCount, (double)mDownloadSizeCounter.get() / (1024d * 1024d));
+                double speed = speedCalculator.feed(mInternetUsageCounter.get()) / ONE_MEGABYTE;
+                if(mUseFileCounter) reportProgressFileCounter(speed);
+                else reportProgressSizeCounter(speed);
             }
             Exception thrownException = mDownloaderThreadException.get();
             if(thrownException != null) {
@@ -123,6 +128,23 @@ private void downloadGame(Activity activity, JMinecraftVersionList.Version verIn
         }
     }
 
+    private void reportProgressFileCounter(double speed) {
+        long dlFileCounter = mProcessedFileCounter.get();
+        int progress = (int)((dlFileCounter * 100L) / mTotalFileCount);
+        ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, progress,
+                R.string.newdl_downloading_game_files, dlFileCounter,
+                mTotalFileCount, speed);
+    }
+
+    private void reportProgressSizeCounter(double speed) {
+        long dlFileSize = mProcessedSizeCounter.get();
+        double dlSizeMegabytes = (double) dlFileSize / ONE_MEGABYTE;
+        double dlTotalMegabytes = (double) mTotalSize / ONE_MEGABYTE;
+        int progress = (int)((dlFileSize * 100L) / mTotalSize);
+        ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, progress,
+                R.string.newdl_downloading_game_files_size, dlSizeMegabytes, dlTotalMegabytes, speed);
+    }
+
     private File createGameJsonPath(String versionId) {
         return new File(Tools.DIR_HOME_VERSION, versionId + File.separator + versionId + ".json");
     }
@@ -233,7 +255,19 @@ private void growDownloadList(int addedElementCount) {
     private void scheduleDownload(File targetFile, int downloadClass, String url, String sha1,
                                   long size, boolean skipIfFailed) throws IOException {
         FileUtils.ensureParentDirectory(targetFile);
-        mDownloadFileCount++;
+        mTotalFileCount++;
+        if(size < 0) {
+            size = DownloadMirror.getContentLengthMirrored(downloadClass, url);
+        }
+        if(size < 0) {
+            // If we were unable to get the content length ourselves, we automatically fall back
+            // to tracking the progress using the file counter.
+            size = 0;
+            mUseFileCounter = true;
+            Log.i("MinecraftDownloader", "Failed to determine size of "+targetFile.getName()+", switching to file counter");
+        }else {
+            mTotalSize += size;
+        }
         mScheduledDownloadTasks.add(
                 new DownloaderTask(targetFile, downloadClass, url, sha1, size, skipIfFailed)
         );
@@ -401,18 +435,20 @@ private void downloadFile() throws Exception {
             }catch (Exception e) {
                 if(!mSkipIfFailed) throw e;
             }
-            mDownloadFileCounter.incrementAndGet();
+            mProcessedFileCounter.incrementAndGet();
         }
 
         private void finishWithoutDownloading() {
-            mDownloadFileCounter.incrementAndGet();
-            mDownloadSizeCounter.addAndGet(mDownloadSize);
+            mProcessedFileCounter.incrementAndGet();
+            mProcessedSizeCounter.addAndGet(mDownloadSize);
         }
 
         @Override
         public void updateProgress(int curr, int max) {
-           mDownloadSizeCounter.addAndGet(curr - mLastCurr);
-           mLastCurr = curr;
+            int delta = curr - mLastCurr;
+            mProcessedSizeCounter.addAndGet(delta);
+            mInternetUsageCounter.addAndGet(delta);
+            mLastCurr = curr;
         }
     }
 }
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/SpeedCalculator.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/SpeedCalculator.java
new file mode 100644
index 0000000000..136b0c1789
--- /dev/null
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/SpeedCalculator.java
@@ -0,0 +1,44 @@
+package net.kdt.pojavlaunch.tasks;
+
+/**
+ * A simple class to calculate the average Internet speed using a simple moving average.
+ */
+public class SpeedCalculator {
+    private long mLastMillis;
+    private long mLastBytes;
+    private int mIndex;
+    private final double[] mPreviousInputs;
+    private double mSum;
+
+    public SpeedCalculator() {
+        this(64);
+    }
+
+    public SpeedCalculator(int averageDepth) {
+        mPreviousInputs = new double[averageDepth];
+    }
+
+    private double addToAverage(double speed) {
+        mSum -= mPreviousInputs[mIndex];
+        mSum += speed;
+        mPreviousInputs[mIndex] = speed;
+        if(++mIndex == mPreviousInputs.length) mIndex = 0;
+        double dLength = mPreviousInputs.length;
+        return (mSum + (dLength / 2d)) / dLength;
+    }
+
+    /**
+     * Update the current amount of bytes downloaded.
+     * @param bytes the new amount of bytes downloaded
+     * @return the current download speed in bytes per second
+     */
+    public double feed(long bytes) {
+        long millis = System.currentTimeMillis();
+        long deltaBytes = bytes - mLastBytes;
+        long deltaMillis = millis - mLastMillis;
+        mLastBytes = bytes;
+        mLastMillis = millis;
+        double speed = (double)deltaBytes / ((double)deltaMillis / 1000d);
+        return addToAverage(speed);
+    }
+}
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/DownloadUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/DownloadUtils.java
index b4df71409a..dbdbadaf07 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/DownloadUtils.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/DownloadUtils.java
@@ -155,6 +155,23 @@ public static <T> T ensureSha1(File outputFile, @Nullable String sha1, Callable<
         return result;
     }
 
+    /**
+     * Get the content length for a given URL.
+     * @param url the URL to get the length for
+     * @return the length in bytes or -1 if not available
+     * @throws IOException if an I/O error occurs.
+     */
+    public static long getContentLength(String url) throws IOException {
+        HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
+        urlConnection.setRequestMethod("HEAD");
+        urlConnection.setDoInput(false);
+        urlConnection.setDoOutput(false);
+        urlConnection.connect();
+        int responseCode = urlConnection.getResponseCode();
+        if(responseCode >= 200 && responseCode <= 299) return urlConnection.getContentLength();
+        return -1;
+    }
+
     public interface ParseCallback<T> {
         T process(String input) throws ParseException;
     }
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 da5cfa9c89..1b9a2cd2b4 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
@@ -215,7 +215,7 @@ public static void setJavaEnvironment(Activity activity, String jreHome) throws
         envMap.put("LD_LIBRARY_PATH", LD_LIBRARY_PATH);
         envMap.put("PATH", jreHome + "/bin:" + Os.getenv("PATH"));
         if(FFmpegPlugin.isAvailable) {
-            envMap.put("PATH", FFmpegPlugin.libraryPath+":"+envMap.get("PATH"));
+            envMap.put("POJAV_FFMPEG_PATH", FFmpegPlugin.executablePath);
         }
 
         if(LOCAL_RENDERER != null) {
@@ -295,6 +295,8 @@ public static void launchJavaVM(final AppCompatActivity activity, final Runtime
         purgeArg(userArgs, "-Dorg.lwjgl.opengl.libname");
         // Don't let the user specify a custom Freetype library (as the user is unlikely to specify a version compiled for Android)
         purgeArg(userArgs, "-Dorg.lwjgl.freetype.libname");
+        // Overridden by us to specify the exact number of cores that the android system has
+        purgeArg(userArgs, "-XX:ActiveProcessorCount");
 
         //Add automatically generated args
         userArgs.add("-Xms" + LauncherPreferences.PREF_RAM_ALLOCATION + "M");
@@ -305,6 +307,9 @@ public static void launchJavaVM(final AppCompatActivity activity, final Runtime
         // that we ship with Java (since it may be older than what's needed)
         userArgs.add("-Dorg.lwjgl.freetype.libname="+ NATIVE_LIB_DIR+"/libfreetype.so");
 
+        // Some phones are not using the right number of cores, fix that
+        userArgs.add("-XX:ActiveProcessorCount=" + java.lang.Runtime.getRuntime().availableProcessors());
+
         userArgs.addAll(JVMArgs);
         activity.runOnUiThread(() -> Toast.makeText(activity, activity.getString(R.string.autoram_info_msg,LauncherPreferences.PREF_RAM_ALLOCATION), Toast.LENGTH_SHORT).show());
         System.out.println(JVMArgs);
@@ -363,7 +368,8 @@ public static List<String> getJavaArgs(Context ctx, String runtimeHome, String u
 
                 "-Dnet.minecraft.clientmodname=" + Tools.APP_NAME,
                 "-Dfml.earlyprogresswindow=false", //Forge 1.14+ workaround
-                "-Dloader.disable_forked_guis=true"
+                "-Dloader.disable_forked_guis=true",
+                "-Djdk.lang.Process.launchMechanism=FORK" // Default is POSIX_SPAWN which requires starting jspawnhelper, which doesn't work on Android
         ));
         if(LauncherPreferences.PREF_ARC_CAPES) {
             overridableArguments.add("-javaagent:"+new File(Tools.DIR_DATA,"arc_dns_injector/arc_dns_injector.jar").getAbsolutePath()+"=23.95.137.176");
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/LocaleUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/LocaleUtils.java
index 82ea22cddb..fb7b53d44d 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/LocaleUtils.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/LocaleUtils.java
@@ -2,6 +2,7 @@
 
 
 import static net.kdt.pojavlaunch.prefs.LauncherPreferences.DEFAULT_PREF;
+import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_FORCE_ENGLISH;
 
 import android.content.*;
 import android.content.res.*;
@@ -24,7 +25,7 @@ public static ContextWrapper setLocale(Context context) {
             LauncherPreferences.loadPreferences(context);
         }
 
-        if(DEFAULT_PREF.getBoolean("force_english", false)){
+        if(PREF_FORCE_ENGLISH){
             Resources resources = context.getResources();
             Configuration configuration = resources.getConfiguration();
 
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 94545abf62..59fccd7764 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
@@ -5,6 +5,9 @@
 @Keep
 public class MinecraftProfile {
 
+	public static String LATEST_RELEASE = "latest-release";
+	public static String LATEST_SNAPSHOT= "latest-snapshot";
+
 	public String name;
 	public String type;
 	public String created;
@@ -23,8 +26,8 @@ public class MinecraftProfile {
 
 	public static MinecraftProfile createTemplate(){
 		MinecraftProfile TEMPLATE = new MinecraftProfile();
-		TEMPLATE.name = "New";
-		TEMPLATE.lastVersionId = "latest-release";
+		TEMPLATE.name = "";
+		TEMPLATE.lastVersionId = LATEST_RELEASE;
 		return TEMPLATE;
 	}
 
diff --git a/app_pojavlauncher/src/main/java/org/lwjgl/glfw/CallbackBridge.java b/app_pojavlauncher/src/main/java/org/lwjgl/glfw/CallbackBridge.java
index ff553bc03b..6a2697c743 100644
--- a/app_pojavlauncher/src/main/java/org/lwjgl/glfw/CallbackBridge.java
+++ b/app_pojavlauncher/src/main/java/org/lwjgl/glfw/CallbackBridge.java
@@ -2,8 +2,10 @@
 
 import net.kdt.pojavlaunch.*;
 import android.content.*;
+import android.util.Log;
 import android.view.Choreographer;
 
+import androidx.annotation.Keep;
 import androidx.annotation.Nullable;
 
 import java.util.ArrayList;
@@ -102,6 +104,7 @@ public static boolean isGrabbing() {
 
     // Called from JRE side
     @SuppressWarnings("unused")
+    @Keep
     public static @Nullable String accessAndroidClipboard(int type, String copy) {
         switch (type) {
             case CLIPBOARD_COPY:
@@ -164,6 +167,7 @@ public static void setModifiers(int keyCode, boolean isDown){
 
     //Called from JRE side
     @SuppressWarnings("unused")
+    @Keep
     private static void onGrabStateChanged(final boolean grabbing) {
         isGrabbing = grabbing;
         sChoreographer.postFrameCallbackDelayed((time) -> {
@@ -190,17 +194,17 @@ public static void removeGrabListener(GrabListener listener) {
         }
     }
 
-    @CriticalNative public static native void nativeSetUseInputStackQueue(boolean useInputStackQueue);
+    @Keep @CriticalNative public static native void nativeSetUseInputStackQueue(boolean useInputStackQueue);
 
-    @CriticalNative private static native boolean nativeSendChar(char codepoint);
+    @Keep @CriticalNative private static native boolean nativeSendChar(char codepoint);
     // GLFW: GLFWCharModsCallback deprecated, but is Minecraft still use?
-    @CriticalNative private static native boolean nativeSendCharMods(char codepoint, int mods);
-    @CriticalNative private static native void nativeSendKey(int key, int scancode, int action, int mods);
+    @Keep @CriticalNative private static native boolean nativeSendCharMods(char codepoint, int mods);
+    @Keep @CriticalNative private static native void nativeSendKey(int key, int scancode, int action, int mods);
     // private static native void nativeSendCursorEnter(int entered);
-    @CriticalNative private static native void nativeSendCursorPos(float x, float y);
-    @CriticalNative private static native void nativeSendMouseButton(int button, int action, int mods);
-    @CriticalNative private static native void nativeSendScroll(double xoffset, double yoffset);
-    @CriticalNative private static native void nativeSendScreenSize(int width, int height);
+    @Keep @CriticalNative private static native void nativeSendCursorPos(float x, float y);
+    @Keep @CriticalNative private static native void nativeSendMouseButton(int button, int action, int mods);
+    @Keep @CriticalNative private static native void nativeSendScroll(double xoffset, double yoffset);
+    @Keep @CriticalNative private static native void nativeSendScreenSize(int width, int height);
     public static native void nativeSetWindowAttrib(int attrib, int value);
     static {
         System.loadLibrary("pojavexec");
diff --git a/app_pojavlauncher/src/main/jni/Android.mk b/app_pojavlauncher/src/main/jni/Android.mk
index f0948a8026..a820a902a8 100644
--- a/app_pojavlauncher/src/main/jni/Android.mk
+++ b/app_pojavlauncher/src/main/jni/Android.mk
@@ -45,6 +45,8 @@ LOCAL_SRC_FILES := \
     jre_launcher.c \
     utils.c \
     stdio_is.c \
+    java_exec_hooks.c \
+    lwjgl_dlopen_hook.c \
     driver_helper/nsbypass.c
 
 ifeq ($(TARGET_ARCH_ABI),arm64-v8a)
diff --git a/app_pojavlauncher/src/main/jni/ctxbridges/gl_bridge.c b/app_pojavlauncher/src/main/jni/ctxbridges/gl_bridge.c
index 89cc44bc79..8f66139735 100644
--- a/app_pojavlauncher/src/main/jni/ctxbridges/gl_bridge.c
+++ b/app_pojavlauncher/src/main/jni/ctxbridges/gl_bridge.c
@@ -55,25 +55,6 @@ static void gl4esi_get_display_dimensions(int* width, int* height) {
     *height = 0;
 }
 
-static bool already_initialized = false;
-static void gl_init_gl4es_internals() {
-    if(already_initialized) return;
-    already_initialized = true;
-    void* gl4es = dlopen("libgl4es_114.so", RTLD_NOLOAD);
-    if(gl4es == NULL) return;
-    void (*set_getmainfbsize)(void (*new_getMainFBSize)(int* width, int* height));
-    set_getmainfbsize = dlsym(gl4es, "set_getmainfbsize");
-    if(set_getmainfbsize == NULL) goto warn;
-    set_getmainfbsize(gl4esi_get_display_dimensions);
-    goto cleanup;
-
-    warn:
-    printf("gl4esinternals warning: gl4es was found but internals not initialized. expect rendering issues.\n");
-    cleanup:
-    // dlclose just decreases a ref counter, so this is fine
-    dlclose(gl4es);
-}
-
 gl_render_window_t* gl_init_context(gl_render_window_t *share) {
     gl_render_window_t* bundle = malloc(sizeof(gl_render_window_t));
     memset(bundle, 0, sizeof(gl_render_window_t));
@@ -145,10 +126,6 @@ void gl_swap_surface(gl_render_window_t* bundle) {
 }
 
 void gl_make_current(gl_render_window_t* bundle) {
-    // Perform initialization here as the renderer may not be loaded when gl_init or gl_init_context is called.
-    // Yes, even though it is dlopened on MC startup by Pojav, due to linker namespacing weirdness
-    // on API 29/MIUI it may not be loaded at the point of the gl_init call in the current namespace.
-    gl_init_gl4es_internals();
 
     if(bundle == NULL) {
         if(eglMakeCurrent_p(g_EglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
@@ -211,3 +188,20 @@ void gl_swap_interval(int swapInterval) {
 
     eglSwapInterval_p(g_EglDisplay, swapInterval);
 }
+
+JNIEXPORT void JNICALL
+Java_org_lwjgl_opengl_PojavRendererInit_nativeInitGl4esInternals(JNIEnv *env, jclass clazz,
+                                                            jobject function_provider) {
+    __android_log_print(ANDROID_LOG_INFO, g_LogTag, "GL4ES internals initializing...");
+    jclass funcProviderClass = (*env)->GetObjectClass(env, function_provider);
+    jmethodID method_getFunctionAddress = (*env)->GetMethodID(env, funcProviderClass, "getFunctionAddress", "(Ljava/lang/CharSequence;)J");
+#define GETSYM(N) ((*env)->CallLongMethod(env, function_provider, method_getFunctionAddress, (*env)->NewStringUTF(env, N)));
+
+    void (*set_getmainfbsize)(void (*new_getMainFBSize)(int* width, int* height)) = (void*)GETSYM("set_getmainfbsize");
+    if(set_getmainfbsize != NULL) {
+        __android_log_print(ANDROID_LOG_INFO, g_LogTag, "GL4ES internals initialized dimension callback");
+        set_getmainfbsize(gl4esi_get_display_dimensions);
+    }
+
+#undef GETSYM
+}
diff --git a/app_pojavlauncher/src/main/jni/egl_bridge.c b/app_pojavlauncher/src/main/jni/egl_bridge.c
index 30b89631bb..8b133a30b2 100644
--- a/app_pojavlauncher/src/main/jni/egl_bridge.c
+++ b/app_pojavlauncher/src/main/jni/egl_bridge.c
@@ -258,14 +258,18 @@ EXTERNAL_API void* pojavCreateContext(void* contextSrc) {
     return br_init_context((basic_render_window_t*)contextSrc);
 }
 
-EXTERNAL_API JNIEXPORT jlong JNICALL
-Java_org_lwjgl_vulkan_VK_getVulkanDriverHandle(ABI_COMPAT JNIEnv *env, ABI_COMPAT jclass thiz) {
-    printf("EGLBridge: LWJGL-side Vulkan loader requested the Vulkan handle\n");
-    // The code below still uses the env var because
+void* maybe_load_vulkan() {
+    // We use the env var because
     // 1. it's easier to do that
     // 2. it won't break if something will try to load vulkan and osmesa simultaneously
     if(getenv("VULKAN_PTR") == NULL) load_vulkan();
-    return strtoul(getenv("VULKAN_PTR"), NULL, 0x10);
+    return (void*) strtoul(getenv("VULKAN_PTR"), NULL, 0x10);
+}
+
+EXTERNAL_API JNIEXPORT jlong JNICALL
+Java_org_lwjgl_vulkan_VK_getVulkanDriverHandle(ABI_COMPAT JNIEnv *env, ABI_COMPAT jclass thiz) {
+    printf("EGLBridge: LWJGL-side Vulkan loader requested the Vulkan handle\n");
+    return (jlong) maybe_load_vulkan();
 }
 
 EXTERNAL_API void pojavSwapInterval(int interval) {
diff --git a/app_pojavlauncher/src/main/jni/input_bridge_v3.c b/app_pojavlauncher/src/main/jni/input_bridge_v3.c
index 4bd94f6e1a..93c1b9041b 100644
--- a/app_pojavlauncher/src/main/jni/input_bridge_v3.c
+++ b/app_pojavlauncher/src/main/jni/input_bridge_v3.c
@@ -30,8 +30,6 @@
 #define EVENT_TYPE_MOUSE_BUTTON 1006
 #define EVENT_TYPE_SCROLL 1007
 
-jint (*orig_ProcessImpl_forkAndExec)(JNIEnv *env, jobject process, jint mode, jbyteArray helperpath, jbyteArray prog, jbyteArray argBlock, jint argc, jbyteArray envBlock, jint envc, jbyteArray dir, jintArray std_fds, jboolean redirectErrorStream);
-
 static void registerFunctions(JNIEnv *env);
 
 jint JNI_OnLoad(JavaVM* vm, __attribute__((unused)) void* reserved) {
@@ -59,7 +57,7 @@ jint JNI_OnLoad(JavaVM* vm, __attribute__((unused)) void* reserved) {
         jobject mouseDownBufferJ = (*pojav_environ->runtimeJNIEnvPtr_JRE)->GetStaticObjectField(pojav_environ->runtimeJNIEnvPtr_JRE, pojav_environ->vmGlfwClass, field_mouseDownBuffer);
         pojav_environ->mouseDownBuffer = (*pojav_environ->runtimeJNIEnvPtr_JRE)->GetDirectBufferAddress(pojav_environ->runtimeJNIEnvPtr_JRE, mouseDownBufferJ);
         hookExec();
-        installLinkerBugMitigation();
+        installLwjglDlopenHook();
         installEMUIIteratorMititgation();
     }
 
@@ -230,82 +228,6 @@ void sendData(int type, int i1, int i2, int i3, int i4) {
     atomic_fetch_add_explicit(&pojav_environ->eventCounter, 1, memory_order_acquire);
 }
 
-/**
- * Hooked version of java.lang.UNIXProcess.forkAndExec()
- * which is used to handle the "open" command.
- */
-jint
-hooked_ProcessImpl_forkAndExec(JNIEnv *env, jobject process, jint mode, jbyteArray helperpath, jbyteArray prog, jbyteArray argBlock, jint argc, jbyteArray envBlock, jint envc, jbyteArray dir, jintArray std_fds, jboolean redirectErrorStream) {
-    char *pProg = (char *)((*env)->GetByteArrayElements(env, prog, NULL));
-
-    // Here we only handle the "xdg-open" command
-    if (strcmp(basename(pProg), "xdg-open") != 0) {
-        (*env)->ReleaseByteArrayElements(env, prog, (jbyte *)pProg, 0);
-        return orig_ProcessImpl_forkAndExec(env, process, mode, helperpath, prog, argBlock, argc, envBlock, envc, dir, std_fds, redirectErrorStream);
-    }
-    (*env)->ReleaseByteArrayElements(env, prog, (jbyte *)pProg, 0);
-
-    Java_org_lwjgl_glfw_CallbackBridge_nativeClipboard(env, NULL, /* CLIPBOARD_OPEN */ 2002, argBlock);
-    return 0;
-}
-
-void hookExec() {
-    jclass cls;
-    orig_ProcessImpl_forkAndExec = dlsym(RTLD_DEFAULT, "Java_java_lang_UNIXProcess_forkAndExec");
-    if (!orig_ProcessImpl_forkAndExec) {
-        orig_ProcessImpl_forkAndExec = dlsym(RTLD_DEFAULT, "Java_java_lang_ProcessImpl_forkAndExec");
-        cls = (*pojav_environ->runtimeJNIEnvPtr_JRE)->FindClass(pojav_environ->runtimeJNIEnvPtr_JRE, "java/lang/ProcessImpl");
-    } else {
-        cls = (*pojav_environ->runtimeJNIEnvPtr_JRE)->FindClass(pojav_environ->runtimeJNIEnvPtr_JRE, "java/lang/UNIXProcess");
-    }
-    JNINativeMethod methods[] = {
-        {"forkAndExec", "(I[B[B[BI[BI[B[IZ)I", (void *)&hooked_ProcessImpl_forkAndExec}
-    };
-    (*pojav_environ->runtimeJNIEnvPtr_JRE)->RegisterNatives(pojav_environ->runtimeJNIEnvPtr_JRE, cls, methods, 1);
-    printf("Registered forkAndExec\n");
-}
-
-/**
- * Basically a verbatim implementation of ndlopen(), found at
- * https://github.com/PojavLauncherTeam/lwjgl3/blob/3.3.1/modules/lwjgl/core/src/generated/c/linux/org_lwjgl_system_linux_DynamicLinkLoader.c#L11
- * The idea is that since, on Android 10 and earlier, the linker doesn't really do namespace nesting.
- * It is not a problem as most of the libraries are in the launcher path, but when you try to run
- * VulkanMod which loads shaderc outside of the default jni libs directory through this method,
- * it can't load it because the path is not in the allowed paths for the anonymous namesapce.
- * This method fixes the issue by being in libpojavexec, and thus being in the classloader namespace
- */
-jlong ndlopen_bugfix(__attribute__((unused)) JNIEnv *env,
-                     __attribute__((unused)) jclass class,
-                     jlong filename_ptr,
-                     jint jmode) {
-    const char* filename = (const char*) filename_ptr;
-    int mode = (int)jmode;
-    return (jlong) dlopen(filename, mode);
-}
-
-/**
- * Install the linker bug mitigation for Android 10 and lower. Fixes VulkanMod crashing on these
- * Android versions due to missing namespace nesting.
- */
-void installLinkerBugMitigation() {
-    if(android_get_device_api_level() >= 30) return;
-    __android_log_print(ANDROID_LOG_INFO, "Api29LinkerFix", "API < 30 detected, installing linker bug mitigation");
-    JNIEnv* env = pojav_environ->runtimeJNIEnvPtr_JRE;
-    jclass dynamicLinkLoader = (*env)->FindClass(env, "org/lwjgl/system/linux/DynamicLinkLoader");
-    if(dynamicLinkLoader == NULL) {
-        __android_log_print(ANDROID_LOG_ERROR, "Api29LinkerFix", "Failed to find the target class");
-        (*env)->ExceptionClear(env);
-        return;
-    }
-    JNINativeMethod ndlopenMethod[] = {
-            {"ndlopen", "(JI)J", &ndlopen_bugfix}
-    };
-    if((*env)->RegisterNatives(env, dynamicLinkLoader, ndlopenMethod, 1) != 0) {
-        __android_log_print(ANDROID_LOG_ERROR, "Api29LinkerFix", "Failed to register the bugfix method");
-        (*env)->ExceptionClear(env);
-    }
-}
-
 /**
  * This function is meant as a substitute for SharedLibraryUtil.getLibraryPath() that just returns 0
  * (thus making the parent Java function return null). This is done to avoid using the LWJGL's default function,
@@ -620,6 +542,7 @@ static bool tryCriticalNative(JNIEnv *env) {
     };
     jclass criticalNativeTest = (*env)->FindClass(env, "net/kdt/pojavlaunch/CriticalNativeTest");
     if(criticalNativeTest == NULL) {
+        LOGD("No CriticalNativeTest class found !");
         (*env)->ExceptionClear(env);
         return false;
     }
diff --git a/app_pojavlauncher/src/main/jni/java_exec_hooks.c b/app_pojavlauncher/src/main/jni/java_exec_hooks.c
new file mode 100644
index 0000000000..4469ba117d
--- /dev/null
+++ b/app_pojavlauncher/src/main/jni/java_exec_hooks.c
@@ -0,0 +1,89 @@
+//
+// Created by maks on 05.01.2025.
+//
+
+#include <jni.h>
+#include <libgen.h>
+#include <string.h>
+#include <stdlib.h>
+#include <dlfcn.h>
+
+#include <environ/environ.h>
+#include <android/log.h>
+#include <utils.h>
+
+static jint (*orig_ProcessImpl_forkAndExec)(JNIEnv *env, jobject process, jint mode, jbyteArray helperpath, jbyteArray prog, jbyteArray argBlock, jint argc, jbyteArray envBlock, jint envc, jbyteArray dir, jintArray std_fds, jboolean redirectErrorStream);
+
+// Turn a C-style string into a Java byte array
+static jbyteArray stringToBytes(JNIEnv *env, const char* string) {
+    const jsize string_data_len = (jsize)(strlen(string) + 1);
+    jbyteArray result = (*env)->NewByteArray(env, (jsize)string_data_len);
+    (*env)->SetByteArrayRegion(env, result, 0, (jsize)string_data_len, (const jbyte*) string);
+    return result;
+}
+
+// Replace the env block with the one that has the desired LD_LIBRARY_PATH/PATH.
+// (Due to my laziness this ignores the current contents of the block)
+static void replaceLibPathInEnvBlock(JNIEnv *env, jbyteArray* envBlock, jint* envc, const char* directory) {
+    static bool env_block_replacement_warning = false;
+    if(*envBlock != NULL && !env_block_replacement_warning) {
+        printf("exec_hooks WARN: replaceLibPathInEnvBlock does not preserve original env. Please notify PojavLauncherTeam if you need that feature\n");
+        env_block_replacement_warning = true;
+    }
+    char envStr[1024];
+    jsize new_envl = snprintf(envStr, sizeof(envStr) / sizeof(char), "LD_LIBRARY_PATH=%s%cPATH=%s", directory, 0 ,directory) + 1;
+    jbyteArray newBlock = (*env)->NewByteArray(env, new_envl);
+    (*env)->SetByteArrayRegion(env, newBlock, 0, new_envl, (jbyte*) envStr);
+    *envBlock = newBlock;
+    *envc = 2;
+}
+
+/**
+ * Hooked version of java.lang.UNIXProcess.forkAndExec()
+ * which is used to handle the "open" command and "ffmpeg" invocations
+ */
+static jint hooked_ProcessImpl_forkAndExec(JNIEnv *env, jobject process, jint mode, jbyteArray helperpath, jbyteArray prog, jbyteArray argBlock, jint argc, jbyteArray envBlock, jint envc, jbyteArray dir, jintArray std_fds, jboolean redirectErrorStream) {
+    const char *pProg = (char *)((*env)->GetByteArrayElements(env, prog, NULL));
+    const char* pProgBaseName = basename(pProg);
+    const size_t basename_len = strlen(pProgBaseName);
+    char prog_basename[basename_len];
+    memcpy(&prog_basename, pProgBaseName, basename_len + 1);
+    (*env)->ReleaseByteArrayElements(env, prog, (jbyte *)pProg, 0);
+
+    if(strcmp(prog_basename, "xdg-open") == 0) {
+        // When invoking xdg-open, send the open URL into Android
+        Java_org_lwjgl_glfw_CallbackBridge_nativeClipboard(env, NULL, CLIPBOARD_OPEN, argBlock);
+        return 0;
+    }else if(strcmp(prog_basename, "ffmpeg") == 0) {
+        // When invoking ffmpeg, always replace the program path with the path to ffmpeg from the plugin.
+        // This allows us to replace the executable name, which is needed because android doesn't allow
+        // us to put files that don't start with "lib" and end with ".so" into folders that we can execute
+        // from
+
+        // Also add LD_LIBRARY_PATH and PATH for the lib in order to override the ones from the launcher, since
+        // they may interfere with ffmpeg dependencies.
+        const char* ffmpeg_path = getenv("POJAV_FFMPEG_PATH");
+        if(ffmpeg_path != NULL) {
+            replaceLibPathInEnvBlock(env, &envBlock, &envc, dirname(ffmpeg_path));
+            prog = stringToBytes(env, ffmpeg_path);
+        }
+    }
+    return orig_ProcessImpl_forkAndExec(env, process, mode, helperpath, prog, argBlock, argc, envBlock, envc, dir, std_fds, redirectErrorStream);
+}
+
+// Hook the forkAndExec method in the Java runtime for custom executable overriding.
+void hookExec() {
+    jclass cls;
+    orig_ProcessImpl_forkAndExec = dlsym(RTLD_DEFAULT, "Java_java_lang_UNIXProcess_forkAndExec");
+    if (!orig_ProcessImpl_forkAndExec) {
+        orig_ProcessImpl_forkAndExec = dlsym(RTLD_DEFAULT, "Java_java_lang_ProcessImpl_forkAndExec");
+        cls = (*pojav_environ->runtimeJNIEnvPtr_JRE)->FindClass(pojav_environ->runtimeJNIEnvPtr_JRE, "java/lang/ProcessImpl");
+    } else {
+        cls = (*pojav_environ->runtimeJNIEnvPtr_JRE)->FindClass(pojav_environ->runtimeJNIEnvPtr_JRE, "java/lang/UNIXProcess");
+    }
+    JNINativeMethod methods[] = {
+            {"forkAndExec", "(I[B[B[BI[BI[B[IZ)I", (void *)&hooked_ProcessImpl_forkAndExec}
+    };
+    (*pojav_environ->runtimeJNIEnvPtr_JRE)->RegisterNatives(pojav_environ->runtimeJNIEnvPtr_JRE, cls, methods, 1);
+    printf("Registered forkAndExec\n");
+}
\ No newline at end of file
diff --git a/app_pojavlauncher/src/main/jni/lwjgl_dlopen_hook.c b/app_pojavlauncher/src/main/jni/lwjgl_dlopen_hook.c
new file mode 100644
index 0000000000..8694613d14
--- /dev/null
+++ b/app_pojavlauncher/src/main/jni/lwjgl_dlopen_hook.c
@@ -0,0 +1,64 @@
+//
+// Created by maks on 06.01.2025.
+//
+
+#include <android/api-level.h>
+#include <android/log.h>
+#include <jni.h>
+
+#include <environ/environ.h>
+
+#include <dlfcn.h>
+#include <string.h>
+#include <stdlib.h>
+
+extern void* maybe_load_vulkan();
+
+/**
+ * Basically a verbatim implementation of ndlopen(), found at
+ * https://github.com/PojavLauncherTeam/lwjgl3/blob/3.3.1/modules/lwjgl/core/src/generated/c/linux/org_lwjgl_system_linux_DynamicLinkLoader.c#L11
+ * but with our own additions for stuff like vulkanmod.
+ */
+static jlong ndlopen_bugfix(__attribute__((unused)) JNIEnv *env,
+                     __attribute__((unused)) jclass class,
+                     jlong filename_ptr,
+                     jint jmode) {
+    const char* filename = (const char*) filename_ptr;
+
+    // Oveeride vulkan loading to let us load vulkan ourselves
+    if(strstr(filename, "libvulkan.so") == filename) {
+        printf("LWJGL linkerhook: replacing load for libvulkan.so with custom driver\n");
+        return (jlong) maybe_load_vulkan();
+    }
+
+    // This hook also serves the task of mitigating a bug: the idea is that since, on Android 10 and
+    // earlier, the linker doesn't really do namespace nesting.
+    // It is not a problem as most of the libraries are in the launcher path, but when you try to run
+    // VulkanMod which loads shaderc outside of the default jni libs directory through this method,
+    // it can't load it because the path is not in the allowed paths for the anonymous namesapce.
+    // This method fixes the issue by being in libpojavexec, and thus being in the classloader namespace
+
+    int mode = (int)jmode;
+    return (jlong) dlopen(filename, mode);
+}
+
+/**
+ * Install the LWJGL dlopen hook. This allows us to mitigate linker bugs and add custom library overrides.
+ */
+void installLwjglDlopenHook() {
+    __android_log_print(ANDROID_LOG_INFO, "LwjglLinkerHook", "Installing LWJGL dlopen() hook");
+    JNIEnv* env = pojav_environ->runtimeJNIEnvPtr_JRE;
+    jclass dynamicLinkLoader = (*env)->FindClass(env, "org/lwjgl/system/linux/DynamicLinkLoader");
+    if(dynamicLinkLoader == NULL) {
+        __android_log_print(ANDROID_LOG_ERROR, "LwjglLinkerHook", "Failed to find the target class");
+        (*env)->ExceptionClear(env);
+        return;
+    }
+    JNINativeMethod ndlopenMethod[] = {
+            {"ndlopen", "(JI)J", &ndlopen_bugfix}
+    };
+    if((*env)->RegisterNatives(env, dynamicLinkLoader, ndlopenMethod, 1) != 0) {
+        __android_log_print(ANDROID_LOG_ERROR, "LwjglLinkerHook", "Failed to register the hooked method");
+        (*env)->ExceptionClear(env);
+    }
+}
\ No newline at end of file
diff --git a/app_pojavlauncher/src/main/jni/utils.h b/app_pojavlauncher/src/main/jni/utils.h
index 4a03c726a2..69583c44eb 100644
--- a/app_pojavlauncher/src/main/jni/utils.h
+++ b/app_pojavlauncher/src/main/jni/utils.h
@@ -2,7 +2,9 @@
 
 #include <stdbool.h>
 
-
+#define CLIPBOARD_COPY 2000
+#define CLIPBOARD_PASTE 2001
+#define CLIPBOARD_OPEN 2002
 
 char** convert_to_char_array(JNIEnv *env, jobjectArray jstringArray);
 jobjectArray convert_from_char_array(JNIEnv *env, char **charArray, int num_rows);
@@ -10,7 +12,7 @@ void free_char_array(JNIEnv *env, jobjectArray jstringArray, const char **charAr
 jstring convertStringJVM(JNIEnv* srcEnv, JNIEnv* dstEnv, jstring srcStr);
 
 void hookExec();
-void installLinkerBugMitigation();
+void installLwjglDlopenHook();
 void installEMUIIteratorMititgation();
 JNIEXPORT jstring JNICALL Java_org_lwjgl_glfw_CallbackBridge_nativeClipboard(JNIEnv* env, jclass clazz, jint action, jbyteArray copySrc);
 
diff --git a/app_pojavlauncher/src/main/res/layout-land/activity_pojav_launcher.xml b/app_pojavlauncher/src/main/res/layout-land/activity_pojav_launcher.xml
index a508abf219..4aea232798 100644
--- a/app_pojavlauncher/src/main/res/layout-land/activity_pojav_launcher.xml
+++ b/app_pojavlauncher/src/main/res/layout-land/activity_pojav_launcher.xml
@@ -28,17 +28,7 @@
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 
-    <ImageButton
-        android:id="@+id/delete_account_button"
-        android:layout_width="@dimen/_52sdp"
-        android:layout_height="@dimen/_42sdp"
-        android:background="?attr/selectableItemBackground"
-        android:src="@drawable/ic_menu_delete_forever"
-        android:scaleType="fitCenter"
-
 
-        app:layout_constraintEnd_toStartOf="@id/setting_button"
-        app:layout_constraintTop_toTopOf="parent" />
 
     <!-- Holding most of the dynamic content -->
     <androidx.fragment.app.FragmentContainerView
diff --git a/app_pojavlauncher/src/main/res/layout/activity_basemain.xml b/app_pojavlauncher/src/main/res/layout/activity_basemain.xml
index a7a95d639a..bd7f5f7a15 100644
--- a/app_pojavlauncher/src/main/res/layout/activity_basemain.xml
+++ b/app_pojavlauncher/src/main/res/layout/activity_basemain.xml
@@ -51,8 +51,9 @@
 
 			<net.kdt.pojavlaunch.customcontrols.handleview.DrawerPullButton
 				android:id="@+id/drawer_button"
-				android:layout_width="24dp"
-				android:layout_height="12dp"
+				android:layout_width="40dp"
+				android:layout_height="20dp"
+				android:padding="8dp"
 				android:elevation="10dp"
 				android:layout_gravity="center_horizontal"/>
 			<net.kdt.pojavlaunch.customcontrols.mouse.HotbarView
diff --git a/app_pojavlauncher/src/main/res/layout/activity_custom_controls.xml b/app_pojavlauncher/src/main/res/layout/activity_custom_controls.xml
index dcde745bbd..2ac37287b7 100644
--- a/app_pojavlauncher/src/main/res/layout/activity_custom_controls.xml
+++ b/app_pojavlauncher/src/main/res/layout/activity_custom_controls.xml
@@ -15,8 +15,9 @@
 
 		<net.kdt.pojavlaunch.customcontrols.handleview.DrawerPullButton
 			android:id="@+id/drawer_button"
-			android:layout_width="32dp"
-			android:layout_height="16dp"
+			android:layout_width="40dp"
+			android:layout_height="20dp"
+			android:padding="6dp"
 			android:elevation="10dp"
 			app:layout_constraintEnd_toEndOf="parent"
 			app:layout_constraintStart_toStartOf="parent"
diff --git a/app_pojavlauncher/src/main/res/layout/activity_pojav_launcher.xml b/app_pojavlauncher/src/main/res/layout/activity_pojav_launcher.xml
index 9ac982c923..b4d20e0378 100644
--- a/app_pojavlauncher/src/main/res/layout/activity_pojav_launcher.xml
+++ b/app_pojavlauncher/src/main/res/layout/activity_pojav_launcher.xml
@@ -28,19 +28,6 @@
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 
-    <ImageButton
-        android:id="@+id/delete_account_button"
-        android:layout_width="@dimen/_52sdp"
-        android:layout_height="@dimen/_52sdp"
-        android:background="?attr/selectableItemBackground"
-        android:src="@drawable/ic_menu_delete_forever"
-        android:scaleType="fitCenter"
-        android:paddingVertical="@dimen/_8sdp"
-
-
-        app:layout_constraintEnd_toStartOf="@id/setting_button"
-        app:layout_constraintTop_toTopOf="parent" />
-
     <!-- Holding most of the dynamic content -->
     <androidx.fragment.app.FragmentContainerView
         android:id="@+id/container_fragment"
diff --git a/app_pojavlauncher/src/main/res/layout/dialog_quick_setting.xml b/app_pojavlauncher/src/main/res/layout/dialog_quick_setting.xml
index 435781a042..b85a83f7c2 100644
--- a/app_pojavlauncher/src/main/res/layout/dialog_quick_setting.xml
+++ b/app_pojavlauncher/src/main/res/layout/dialog_quick_setting.xml
@@ -26,6 +26,8 @@
         android:id="@+id/editResolution_seekbar"
         android:layout_width="0dp"
         android:layout_height="@dimen/_36sdp"
+        android:min="@integer/resolution_seekbar_min"
+        app:seekBarIncrement="@integer/resolution_seekbar_increment"
 
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintHorizontal_bias="0.538"
@@ -34,9 +36,9 @@
 
     <TextView
         android:id="@+id/editResolution_textView_percent"
-        android:layout_width="50dp"
+        android:layout_width="150dp"
         android:layout_height="0dp"
-        android:gravity="center"
+        android:gravity="end"
         tools:text="50%"
 
         app:layout_constraintBottom_toBottomOf="@id/editResolution_textView"
@@ -92,6 +94,10 @@
         android:layout_width="0dp"
         android:layout_height="@dimen/_36sdp"
 
+        android:min="@integer/gyro_speed_seekbar_min"
+        android:max="@integer/gyro_speed_seekbar_max"
+        app:seekBarIncrement="@integer/gyro_speed_seekbar_increment"
+
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintHorizontal_bias="0.538"
         app:layout_constraintStart_toStartOf="parent"
@@ -99,9 +105,9 @@
 
     <TextView
         android:id="@+id/editGyro_textView_percent"
-        android:layout_width="50dp"
+        android:layout_width="150dp"
         android:layout_height="0dp"
-        android:gravity="center"
+        android:gravity="end"
         tools:text="100%"
 
         app:layout_constraintBottom_toBottomOf="@id/editGyro_textView"
@@ -127,6 +133,10 @@
         android:layout_width="0dp"
         android:layout_height="@dimen/_36sdp"
 
+        android:min="@integer/mouse_speed_seekbar_min"
+        android:max="@integer/mouse_speed_seekbar_max"
+        app:seekBarIncrement="@integer/mouse_speed_seekbar_increment"
+
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintHorizontal_bias="0.538"
         app:layout_constraintStart_toStartOf="parent"
@@ -134,9 +144,9 @@
 
     <TextView
         android:id="@+id/editMouseSpeed_textView_percent"
-        android:layout_width="50dp"
+        android:layout_width="150dp"
         android:layout_height="0dp"
-        android:gravity="center"
+        android:gravity="end"
         tools:text="100%"
 
         app:layout_constraintBottom_toBottomOf="@id/editMouseSpeed_textView"
@@ -172,6 +182,10 @@
         android:layout_width="0dp"
         android:layout_height="@dimen/_36sdp"
 
+        android:min="@integer/gesture_delay_seekbar_min"
+        android:max="@integer/gesture_delay_seekbar_max"
+        app:seekBarIncrement="@integer/gesture_delay_seekbar_increment"
+
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintHorizontal_bias="0.538"
         app:layout_constraintStart_toStartOf="parent"
@@ -179,10 +193,10 @@
 
     <TextView
         android:id="@+id/editGestureDelay_textView_percent"
-        android:layout_width="50dp"
+        android:layout_width="150dp"
         android:layout_height="0dp"
-        android:gravity="center"
-        tools:text="270ms"
+        android:gravity="end"
+        tools:text="270 ms"
 
         app:layout_constraintBottom_toBottomOf="@id/editGestureDelay_textView"
         app:layout_constraintEnd_toEndOf="parent"
diff --git a/app_pojavlauncher/src/main/res/layout/item_minecraft_account.xml b/app_pojavlauncher/src/main/res/layout/item_minecraft_account.xml
index 9fa49bc0ad..23049ec4d3 100644
--- a/app_pojavlauncher/src/main/res/layout/item_minecraft_account.xml
+++ b/app_pojavlauncher/src/main/res/layout/item_minecraft_account.xml
@@ -1,17 +1,39 @@
-<fr.spse.extended_view.ExtendedTextView
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_height="@dimen/_52sdp"
     android:layout_width="match_parent"
-    android:textSize="@dimen/_16ssp"
-    android:gravity="center_vertical"
-    android:drawablePadding="@dimen/_6sdp"
-    android:paddingStart="@dimen/_8sdp"
-    app:drawableStartSize="@dimen/_30sdp"
+    android:orientation="horizontal">
 
-    tools:text="HELLO THERE"
-    tools:drawableStart="@mipmap/ic_launcher"
+    <fr.spse.extended_view.ExtendedTextView
+        android:id="@+id/account_item"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
 
-    tools:ignore="RtlSymmetry" />
+        android:textSize="@dimen/_16ssp"
+        android:gravity="center_vertical"
+        android:drawablePadding="@dimen/_6sdp"
+        android:paddingStart="@dimen/_8sdp"
+        app:drawableStartSize="@dimen/_30sdp"
+
+        tools:text="HELLO THERE"
+        tools:drawableStart="@mipmap/ic_launcher"
+
+
+        tools:ignore="RtlSymmetry" />
+
+    <!-- Fun fact, if I put an Image button, the spinner fails to put an onclick listener on the extended view -->
+    <ImageView
+        android:id="@+id/delete_account_button"
+        android:layout_width="@dimen/_52sdp"
+        android:layout_height="match_parent"
+        android:background="?attr/selectableItemBackground"
+        android:src="@drawable/ic_menu_delete_forever"
+
+        android:padding="@dimen/padding_moderate"
+        />
+
+</LinearLayout>
 
diff --git a/app_pojavlauncher/src/main/res/values/attributes.xml b/app_pojavlauncher/src/main/res/values/attributes.xml
new file mode 100644
index 0000000000..9b8b1b06a3
--- /dev/null
+++ b/app_pojavlauncher/src/main/res/values/attributes.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <attr name="seekBarIncrement" format="integer" />
+    <declare-styleable name="CustomSeekbar">
+        <attr name="seekBarIncrement" />
+        <!-- Redefine to be able to fetch the value -->
+        <attr name="android:min" />
+    </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml
index cfc5affce1..8814ef333a 100644
--- a/app_pojavlauncher/src/main/res/values/strings.xml
+++ b/app_pojavlauncher/src/main/res/values/strings.xml
@@ -323,6 +323,7 @@
 
     <string name="generic_install">Install</string>
     <string name="generic_apply">Apply</string>
+    <string name="generic_debug">Debug</string>
 
     <string name="search_modpack_no_result">No modpacks found</string>
     <string name="search_modpack_error">Failed to find modpacks</string>
@@ -373,7 +374,8 @@
     <string name="exception_failed_to_unpack_jre17">Failed to install JRE 17</string>
     <string name="newdl_starting">Reading game metadata…</string>
     <string name="newdl_downloading_metadata">Downloading game metadata (%s)</string>
-    <string name="newdl_downloading_game_files">Downloading game files… (%d/%d, %.2f MB)</string>
+    <string name="newdl_downloading_game_files">Downloading game… (%d/%d, %.2f MB/s)</string>
+    <string name="newdl_downloading_game_files_size">Downloading game… (%.2f/%.2f MB, %.2f MB/s)</string>
     <string name="cropper_title">Select image region</string>
     <string name="cropper_lock_vertical">V. lock</string>
     <string name="cropper_lock_horizontal">H. lock</string>
diff --git a/app_pojavlauncher/src/main/res/values/values.xml b/app_pojavlauncher/src/main/res/values/values.xml
new file mode 100644
index 0000000000..3220345b68
--- /dev/null
+++ b/app_pojavlauncher/src/main/res/values/values.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <integer name="resolution_seekbar_increment">5</integer>
+    <integer name="resolution_seekbar_min">25</integer>
+
+    <integer name="gesture_delay_seekbar_increment">10</integer>
+    <integer name="gesture_delay_seekbar_min">100</integer>
+    <integer name="gesture_delay_seekbar_max">1000</integer>
+
+
+    <integer name="button_scale_seekbar_increment">5</integer>
+    <integer name="button_scale_seekbar_min">80</integer>
+    <integer name="button_scale_seekbar_max">250</integer>
+
+    <integer name="mouse_scale_seekbar_increment">5</integer>
+    <integer name="mouse_scale_seekbar_min">25</integer>
+    <integer name="mouse_scale_seekbar_max">300</integer>
+
+    <integer name="mouse_speed_seekbar_increment">5</integer>
+    <integer name="mouse_speed_seekbar_min">25</integer>
+    <integer name="mouse_speed_seekbar_max">300</integer>
+
+    <integer name="gyro_speed_seekbar_increment">5</integer>
+    <integer name="gyro_speed_seekbar_min">25</integer>
+    <integer name="gyro_speed_seekbar_max">300</integer>
+
+    <integer name="gyro_rate_seekbar_min">5</integer>
+    <integer name="gyro_rate_seekbar_max">50</integer>
+
+    <integer name="gamepad_deadzone_seekbar_increment">5</integer>
+    <integer name="gamepad_deadzone_seekbar_min">50</integer>
+    <integer name="gamepad_deadzone_seekbar_max">200</integer>
+
+    <integer name="memory_seekbar_increment">8</integer>
+    <integer name="memory_seekbar_min">256</integer>
+
+</resources>
\ No newline at end of file
diff --git a/app_pojavlauncher/src/main/res/xml/pref_control.xml b/app_pojavlauncher/src/main/res/xml/pref_control.xml
index f6300e7c32..0d2778e173 100644
--- a/app_pojavlauncher/src/main/res/xml/pref_control.xml
+++ b/app_pojavlauncher/src/main/res/xml/pref_control.xml
@@ -35,7 +35,9 @@
             android:title="@string/mcl_setting_title_longpresstrigger"
             app2:showSeekBarValue="true"
             app2:selectable="false"
-            app2:seekBarIncrement="10"
+            app2:min="@integer/gesture_delay_seekbar_min"
+            android:max="@integer/gesture_delay_seekbar_max"
+            app2:seekBarIncrement="@integer/gesture_delay_seekbar_increment"
             android:icon="@drawable/ic_setting_gesture_time"
             />
 
@@ -50,7 +52,9 @@
             android:summary="@string/mcl_setting_subtitle_buttonscale"
             app2:showSeekBarValue="true"
             app2:selectable="false"
-            app2:seekBarIncrement="5"
+            app2:min="@integer/button_scale_seekbar_min"
+            android:max="@integer/button_scale_seekbar_max"
+            app2:seekBarIncrement="@integer/button_scale_seekbar_increment"
             android:icon="@drawable/ic_setting_control_scale"
             />
         <SwitchPreference
@@ -71,7 +75,9 @@
             android:title="@string/mcl_setting_title_mousescale"
 
             app2:selectable="false"
-            app2:seekBarIncrement="5"
+            app2:min="@integer/mouse_scale_seekbar_min"
+            android:max="@integer/mouse_scale_seekbar_max"
+            app2:seekBarIncrement="@integer/mouse_scale_seekbar_increment"
             app2:showSeekBarValue="true"
             android:icon="@drawable/ic_setting_pointer_scale"
             />
@@ -82,7 +88,9 @@
             android:title="@string/mcl_setting_title_mousespeed"
             android:icon="@drawable/ic_setting_mouse_speed"
             app2:selectable="false"
-            app2:seekBarIncrement="5"
+            app2:min="@integer/mouse_speed_seekbar_min"
+            android:max="@integer/mouse_speed_seekbar_max"
+            app2:seekBarIncrement="@integer/mouse_speed_seekbar_increment"
             app2:showSeekBarValue="true" />
         <SwitchPreference
             android:key="mouse_start"
@@ -104,12 +112,16 @@
             android:title="@string/preference_gyro_sensitivity_title"
             android:summary="@string/preference_gyro_sensitivity_description"
             app2:selectable="false"
-            app2:seekBarIncrement="5"
+            app2:min="@integer/gyro_speed_seekbar_min"
+            android:max="@integer/gyro_speed_seekbar_max"
+            app2:seekBarIncrement="@integer/gyro_speed_seekbar_increment"
             app2:showSeekBarValue="true"/>
         <net.kdt.pojavlaunch.prefs.CustomSeekBarPreference
             android:key="gyroSampleRate"
             android:title="@string/preference_gyro_sample_rate_title"
             android:summary="@string/preference_gyro_sample_rate_description"
+            app2:min="@integer/gyro_rate_seekbar_min"
+            android:max="@integer/gyro_rate_seekbar_max"
             app2:selectable="false"
             app2:showSeekBarValue="true"/>
         <SwitchPreferenceCompat
@@ -145,7 +157,9 @@
             android:title="@string/preference_deadzone_scale_title"
             android:summary="@string/preference_deadzone_scale_description"
             app2:showSeekBarValue="true"
-            app2:seekBarIncrement="5"
+            app2:min="@integer/gamepad_deadzone_seekbar_min"
+            android:max="@integer/gamepad_deadzone_seekbar_max"
+            app2:seekBarIncrement="@integer/gamepad_deadzone_seekbar_increment"
             />
 
     </PreferenceCategory>
diff --git a/app_pojavlauncher/src/main/res/xml/pref_java.xml b/app_pojavlauncher/src/main/res/xml/pref_java.xml
index 46616ae4d8..6a23c06ff5 100644
--- a/app_pojavlauncher/src/main/res/xml/pref_java.xml
+++ b/app_pojavlauncher/src/main/res/xml/pref_java.xml
@@ -25,7 +25,8 @@
             android:summary="@string/mcl_memory_allocation_subtitle"
             android:title="@string/mcl_memory_allocation"
             app2:showSeekBarValue="true"
-            app2:seekBarIncrement="8"
+            app2:min="@integer/memory_seekbar_min"
+            app2:seekBarIncrement="@integer/memory_seekbar_increment"
             app2:selectable="false"/>
 
         <SwitchPreference
diff --git a/app_pojavlauncher/src/main/res/xml/pref_video.xml b/app_pojavlauncher/src/main/res/xml/pref_video.xml
index 3748f24e76..76032bd0a4 100644
--- a/app_pojavlauncher/src/main/res/xml/pref_video.xml
+++ b/app_pojavlauncher/src/main/res/xml/pref_video.xml
@@ -26,7 +26,8 @@
             android:title="@string/mcl_setting_title_resolution_scaler"
             app2:showSeekBarValue="true"
             app2:selectable="false"
-            app2:seekBarIncrement="5"
+            app2:min="@integer/resolution_seekbar_min"
+            app2:seekBarIncrement="@integer/resolution_seekbar_increment"
             android:icon="@drawable/ic_setting_screen_resolution"
             />
 
diff --git a/gradle.properties b/gradle.properties
index 7be9b6b006..d3bacf9dcf 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -5,3 +5,5 @@ android.useAndroidX=true
 android.bundle.language.enableSplit=false
 # Increase Gradle daemon RAM allocation
 org.gradle.jvmargs=-Xmx4096M
+android.nonTransitiveRClass=false
+android.nonFinalResIds=false
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index ccebba7710..d64cd49177 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 508322917b..84410b7f77 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,8 @@
+#Fri Dec 06 21:56:04 CET 2024
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
 networkTimeout=10000
+validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 79a61d421c..1aa94a4269 100755
--- a/gradlew
+++ b/gradlew
@@ -83,10 +83,8 @@ done
 # This is normally unused
 # shellcheck disable=SC2034
 APP_BASE_NAME=${0##*/}
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD=maximum
@@ -133,10 +131,13 @@ location of your Java installation."
     fi
 else
     JAVACMD=java
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
+    fi
 fi
 
 # Increase the maximum file descriptors if we can.
@@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
     case $MAX_FD in #(
       max*)
         # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
-        # shellcheck disable=SC3045
+        # shellcheck disable=SC2039,SC3045
         MAX_FD=$( ulimit -H -n ) ||
             warn "Could not query maximum file descriptor limit"
     esac
@@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
       '' | soft) :;; #(
       *)
         # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
-        # shellcheck disable=SC3045
+        # shellcheck disable=SC2039,SC3045
         ulimit -n "$MAX_FD" ||
             warn "Could not set maximum file descriptor limit to $MAX_FD"
     esac
@@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then
     done
 fi
 
-# Collect all arguments for the java command;
-#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-#     shell script including quotes and variable substitutions, so put them in
-#     double quotes to make sure that they get re-expanded; and
-#   * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
 
 set -- \
         "-Dorg.gradle.appname=$APP_BASE_NAME" \
diff --git a/jre_lwjgl3glfw/src/main/java/org/lwjgl/opengl/GLCapabilities.java b/jre_lwjgl3glfw/src/main/java/org/lwjgl/opengl/GLCapabilities.java
index 861187a512..034e4e6bd5 100644
--- a/jre_lwjgl3glfw/src/main/java/org/lwjgl/opengl/GLCapabilities.java
+++ b/jre_lwjgl3glfw/src/main/java/org/lwjgl/opengl/GLCapabilities.java
@@ -4792,6 +4792,8 @@ public final class GLCapabilities {
     GLCapabilities(FunctionProvider provider, Set<String> ext, boolean fc, IntFunction<PointerBuffer> bufferFactory) {
         forwardCompatible = fc;
 
+        PojavRendererInit.onCreateCapabilities(provider);
+
         PointerBuffer caps = bufferFactory.apply(ADDRESS_BUFFER_SIZE);
 
         OpenGL11 = check_GL11(provider, caps, ext, fc);
diff --git a/jre_lwjgl3glfw/src/main/java/org/lwjgl/opengl/PojavRendererInit.java b/jre_lwjgl3glfw/src/main/java/org/lwjgl/opengl/PojavRendererInit.java
new file mode 100644
index 0000000000..8a74020a72
--- /dev/null
+++ b/jre_lwjgl3glfw/src/main/java/org/lwjgl/opengl/PojavRendererInit.java
@@ -0,0 +1,39 @@
+package org.lwjgl.opengl;
+
+import org.lwjgl.system.FunctionProvider;
+import org.lwjgl.system.SharedLibrary;
+
+import javax.annotation.Nullable;
+
+/**
+ * Class for initializing renderer-specific callbacks. Allows to reliably initialize
+ * any callbacks needed for renderers by using the same FunctionProvider as used for loading
+ * GL symbols.
+ * */
+public class PojavRendererInit {
+
+    public static void onCreateCapabilities(FunctionProvider functionProvider) {
+        String rendererName = null;
+        if(functionProvider instanceof SharedLibrary) {
+            SharedLibrary rendererLibrary = (SharedLibrary) functionProvider;
+            rendererName = rendererLibrary.getName();
+        }
+        if(!isValidString(rendererName)) {
+            rendererName = System.getProperty("org.lwjgl.opengl.libname");
+        }
+        if(!isValidString(rendererName)) {
+            System.out.println("PojavRendererInit: Failed to find Pojav renderer name! " +
+                                "Renderer-specific initialization may not work properly");
+        }
+        // NOTE: hardcoded gl4es libname
+        if(rendererName.endsWith("libgl4es_114.so")) {
+            nativeInitGl4esInternals(functionProvider);
+        }
+    }
+
+    private static boolean isValidString(@Nullable  String s) {
+        return s != null && !s.isEmpty();
+    }
+
+    public static native void nativeInitGl4esInternals(FunctionProvider functionProvider);
+}


Test patch: