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/app_pojavlauncher/src/main/java/com/kdt/CustomSeekbar.java b/app_pojavlauncher/src/main/java/com/kdt/CustomSeekbar.java index be80f33a88..5a240ee9f3 100644 --- a/app_pojavlauncher/src/main/java/com/kdt/CustomSeekbar.java +++ b/app_pojavlauncher/src/main/java/com/kdt/CustomSeekbar.java @@ -3,6 +3,7 @@ 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; @@ -19,8 +20,53 @@ 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); @@ -37,11 +83,6 @@ public CustomSeekbar(Context context, AttributeSet attrs, int defStyleAttr) { setup(attrs); } - public CustomSeekbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - setup(attrs); - } - public void setIncrement(int increment) { mIncrement = increment; } @@ -68,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 */ @@ -82,54 +128,25 @@ public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) { public void setup(@Nullable AttributeSet attrs) { try (TypedArray attributes = getContext().obtainStyledAttributes(attrs, R.styleable.CustomSeekbar)) { - mIncrement = attributes.getInt(R.styleable.CustomSeekbar_seekBarIncrement, 1); - } - - 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; + 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()); + } - @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; - } - }); + // 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/net/kdt/pojavlaunch/Tools.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java index beb826c564..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,6 +27,7 @@ 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; @@ -40,7 +41,6 @@ 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; @@ -513,33 +513,7 @@ 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) { + public static void setFullscreen(Activity activity, boolean fullscreen) { WindowInsetsControllerCompat windowInsetsController = WindowCompat.getInsetsController(activity.getWindow(), activity.getWindow().getDecorView()); if (windowInsetsController == null) { @@ -555,27 +529,24 @@ private static void setFullscreenSdk30(Activity activity, boolean fullscreen) { ViewCompat.setOnApplyWindowInsetsListener( activity.getWindow().getDecorView(), (view, windowInsets) -> { - if (fullscreen && !activity.isInMultiWindowMode()) { + boolean fullscreenImpl = fullscreen; + if (SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode()) + fullscreenImpl = false; + + if (fullscreenImpl) { windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()); - activity.getWindow().setDecorFitsSystemWindows(false); } else { windowInsetsController.show(WindowInsetsCompat.Type.systemBars()); - activity.getWindow().setDecorFitsSystemWindows(true); } + if(SDK_INT >= Build.VERSION_CODES.R) + activity.getWindow().setDecorFitsSystemWindows(!fullscreenImpl); + return ViewCompat.onApplyWindowInsets(view, windowInsets); }); } - public static void setFullscreen(Activity activity, boolean fullscreen) { - if (SDK_INT >= Build.VERSION_CODES.R) { - setFullscreenSdk30(activity, fullscreen); - }else { - setFullscreenLegacy(activity, fullscreen); - } - } - public static DisplayMetrics currentDisplayMetrics; public static void updateWindowSize(Activity activity) { 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 38ffef803b..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 @@ -17,7 +17,7 @@ public class LeftClickGesture extends ValidatorGesture { private boolean mMouseActivated; public LeftClickGesture(Handler handler) { - super(handler, LauncherPreferences.PREF_LONGPRESS_TRIGGER); + super(handler); } public final void inputEvent() { @@ -27,6 +27,11 @@ public final void inputEvent() { } } + @Override + protected int getGestureDelay() { + return LauncherPreferences.PREF_LONGPRESS_TRIGGER; + } + @Override public boolean checkAndTrigger() { boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY, mGestureEndX, mGestureEndY, FINGER_STILL_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 d24874f7ed..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,29 +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, 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. @@ -44,6 +50,7 @@ public void onGestureCancelled(boolean isSwitching) { mGestureEnabled = true; if(!mGestureValid || isSwitching) return; 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/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/QuickSettingSideDialog.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/QuickSettingSideDialog.java index 3cf0131dfd..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,7 +122,6 @@ private void setupListeners() { mEditor.putBoolean("disableGestures", isChecked); }); - mGyroSensitivityBar.setRange(25, 300); mGyroSensitivityBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> { PREF_GYRO_SENSITIVITY = progress / 100f; mEditor.putInt("gyroSensitivity", progress); @@ -131,7 +130,6 @@ private void setupListeners() { mGyroSensitivityBar.setProgress((int) (mOriginalGyroSensitivity * 100f)); setSeekTextPercent(mGyroSensitivityText, mGyroSensitivityBar.getProgress()); - mMouseSpeedBar.setRange(25, 300); mMouseSpeedBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> { PREF_MOUSESPEED = progress / 100f; mEditor.putInt("mousespeed", progress); @@ -140,7 +138,6 @@ private void setupListeners() { mMouseSpeedBar.setProgress((int) (mOriginalMouseSpeed * 100f)); setSeekTextPercent(mMouseSpeedText, mMouseSpeedBar.getProgress()); - mGestureDelayBar.setRange(100, 1000); mGestureDelayBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> { PREF_LONGPRESS_TRIGGER = progress; mEditor.putInt("timeLongPressTrigger", progress); @@ -149,7 +146,6 @@ private void setupListeners() { mGestureDelayBar.setProgress(mOriginalGestureDelay); setSeekTextMillisecond(mGestureDelayText, mGestureDelayBar.getProgress()); - mResolutionBar.setRange(25, 100); 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 4b2bc5279b..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 @@ -27,7 +27,6 @@ public void onCreatePreferences(Bundle b, String str) { CustomSeekBarPreference resolutionSeekbar = requirePreference("resolutionRatio", CustomSeekBarPreference.class); - resolutionSeekbar.setMin(25); resolutionSeekbar.setSuffix(" %"); // #724 bug fix 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 mDownloaderThreadException; private ArrayList 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 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 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 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 521c91de84..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) { @@ -368,7 +368,8 @@ public static List 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/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 03bb79f627..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, 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 +#include +#include +#include +#include + +#include +#include +#include + +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 +#include +#include + +#include + +#include +#include +#include + +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 - +#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/dialog_quick_setting.xml b/app_pojavlauncher/src/main/res/layout/dialog_quick_setting.xml index 211e63dd7e..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,7 +26,7 @@ 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" @@ -36,9 +36,9 @@ + + \ 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 91104432dc..8814ef333a 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -374,7 +374,8 @@ Failed to install JRE 17 Reading game metadata… Downloading game metadata (%s) - Downloading game files… (%d/%d, %.2f MB) + Downloading game… (%d/%d, %.2f MB/s) + Downloading game… (%.2f/%.2f MB, %.2f MB/s) Select image region V. lock H. lock diff --git a/app_pojavlauncher/src/main/res/values/values.xml b/app_pojavlauncher/src/main/res/values/values.xml index af6989ff31..3220345b68 100644 --- a/app_pojavlauncher/src/main/res/values/values.xml +++ b/app_pojavlauncher/src/main/res/values/values.xml @@ -1,11 +1,37 @@ 5 + 25 + 10 + 100 + 1000 + + 5 + 80 + 250 + 5 + 25 + 300 + 5 + 25 + 300 + 5 + 25 + 300 + + 5 + 50 + 5 + 50 + 200 + 8 + 256 + \ 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 e892adcfad..0d2778e173 100644 --- a/app_pojavlauncher/src/main/res/xml/pref_control.xml +++ b/app_pojavlauncher/src/main/res/xml/pref_control.xml @@ -35,6 +35,8 @@ android:title="@string/mcl_setting_title_longpresstrigger" app2:showSeekBarValue="true" app2:selectable="false" + 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,6 +52,8 @@ android:summary="@string/mcl_setting_subtitle_buttonscale" app2:showSeekBarValue="true" app2:selectable="false" + 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" /> @@ -71,6 +75,8 @@ android:title="@string/mcl_setting_title_mousescale" app2:selectable="false" + 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,6 +88,8 @@ android:title="@string/mcl_setting_title_mousespeed" android:icon="@drawable/ic_setting_mouse_speed" app2:selectable="false" + app2:min="@integer/mouse_speed_seekbar_min" + android:max="@integer/mouse_speed_seekbar_max" app2:seekBarIncrement="@integer/mouse_speed_seekbar_increment" app2:showSeekBarValue="true" /> diff --git a/app_pojavlauncher/src/main/res/xml/pref_java.xml b/app_pojavlauncher/src/main/res/xml/pref_java.xml index 239836cd7d..6a23c06ff5 100644 --- a/app_pojavlauncher/src/main/res/xml/pref_java.xml +++ b/app_pojavlauncher/src/main/res/xml/pref_java.xml @@ -25,6 +25,7 @@ android:summary="@string/mcl_memory_allocation_subtitle" android:title="@string/mcl_memory_allocation" app2:showSeekBarValue="true" + app2:min="@integer/memory_seekbar_min" app2:seekBarIncrement="@integer/memory_seekbar_increment" app2:selectable="false"/> diff --git a/app_pojavlauncher/src/main/res/xml/pref_video.xml b/app_pojavlauncher/src/main/res/xml/pref_video.xml index e791b9885e..76032bd0a4 100644 --- a/app_pojavlauncher/src/main/res/xml/pref_video.xml +++ b/app_pojavlauncher/src/main/res/xml/pref_video.xml @@ -26,6 +26,7 @@ android:title="@string/mcl_setting_title_resolution_scaler" app2:showSeekBarValue="true" app2:selectable="false" + app2:min="@integer/resolution_seekbar_min" app2:seekBarIncrement="@integer/resolution_seekbar_increment" android:icon="@drawable/ic_setting_screen_resolution" /> 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 ext, boolean fc, IntFunction 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); +}