From 29605ed6b75e1aabb74dc92b918e404ec811a7ec Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Fri, 29 Nov 2024 18:22:39 +0100 Subject: [PATCH 01/34] QoL: make the settings button easier to click Bigger hitbox, but not visually --- .../customcontrols/handleview/DrawerPullButton.java | 11 +++++------ .../src/main/res/layout/activity_basemain.xml | 5 +++-- .../src/main/res/layout/activity_custom_controls.xml | 5 +++-- 3 files changed, 11 insertions(+), 10 deletions(-) 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/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 @@ Date: Fri, 29 Nov 2024 18:57:24 +0100 Subject: [PATCH 02/34] QoL(profile editor): make the text react like a button This increases the space you can use, better for left handed people or large hands --- .../fragments/ProfileEditorFragment.java | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) 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()); + }); } From 68fa25cafc6bbfd379bb03e9a39e87942abe64af Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Fri, 29 Nov 2024 21:14:16 +0100 Subject: [PATCH 03/34] Fix(hotbar): first touch on 0 index not being taken into account --- .../customcontrols/mouse/HotbarView.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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() { From c6fe3c3de5cbda3e729884dccce2b5a15110fcb9 Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Fri, 29 Nov 2024 21:17:29 +0100 Subject: [PATCH 04/34] QoL(gesture): decouple gyroscope from long press gesture --- .../mouse/InGameEventProcessor.java | 8 +++++-- .../mouse/LeftClickGesture.java | 22 +++++++++++++++---- .../mouse/RightClickGesture.java | 9 ++++++-- 3 files changed, 31 insertions(+), 8 deletions(-) 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..38ffef803b 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,7 +13,7 @@ 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) { @@ -22,14 +22,14 @@ public LeftClickGesture(Handler handler) { public final void inputEvent() { if(submit()) { - mGestureStartX = CallbackBridge.mouseX; - mGestureStartY = CallbackBridge.mouseY; + mGestureStartX = mGestureEndX = CallbackBridge.mouseX; + mGestureStartY = mGestureEndY = CallbackBridge.mouseY; } } @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 +47,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 +66,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..d24874f7ed 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 @@ -9,7 +9,7 @@ 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); } @@ -24,6 +24,11 @@ public final void inputEvent() { } } + public void setMotion(float deltaX, float deltaY) { + mGestureEndX += deltaX; + mGestureEndY += deltaY; + } + @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 +43,7 @@ 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); if(!fingerStill) return; CallbackBridge.sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, true); CallbackBridge.sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, false); From fc81b87e7fdcd820f771dd4393188c2626774dda Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Sun, 1 Dec 2024 23:29:16 +0100 Subject: [PATCH 05/34] Fix(controls): non square joystick, set size via text --- app_pojavlauncher/build.gradle | 2 +- .../customcontrols/CustomControls.java | 6 +-- .../customcontrols/LayoutConverter.java | 53 ++++++++++++++----- .../handleview/EditControlSideDialog.java | 9 ++++ 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/app_pojavlauncher/build.gradle b/app_pojavlauncher/build.gradle index 3654bba93c..19bcb89309 100644 --- a/app_pojavlauncher/build.gradle +++ b/app_pojavlauncher/build.gradle @@ -204,7 +204,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/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/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(); } }); From bc7dfeacfdcf601bce962d1191ee442e79c586bb Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Tue, 3 Dec 2024 21:04:09 +0100 Subject: [PATCH 06/34] Fix(control-editors): make sure the layout is instantiated before using it --- app_pojavlauncher/src/main/java/com/kdt/SideDialogView.java | 5 +++++ 1 file changed, 5 insertions(+) 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(); From 1af64382eb45924a2214aac42ac5465bcb7fef1e Mon Sep 17 00:00:00 2001 From: Mathias Boulay Date: Fri, 6 Dec 2024 21:04:17 +0100 Subject: [PATCH 07/34] QoL update (#6350) * QoL(notification): click to go back into the current game * Fix(system bars): fix colors for navigation bars * Tweak(control editor): make snapping less aggressive * Fix(system bars): properly remove colors in full screen --- .../src/main/AndroidManifest.xml | 2 +- .../net/kdt/pojavlaunch/LauncherActivity.java | 12 ++-- .../main/java/net/kdt/pojavlaunch/Tools.java | 62 +++++++++---------- .../customcontrols/ControlData.java | 3 +- .../customcontrols/buttons/ControlDrawer.java | 9 +-- .../buttons/ControlInterface.java | 13 +++- .../kdt/pojavlaunch/services/GameService.java | 6 ++ 7 files changed, 60 insertions(+), 47 deletions(-) diff --git a/app_pojavlauncher/src/main/AndroidManifest.xml b/app_pojavlauncher/src/main/AndroidManifest.xml index 98628756a7..740e44964b 100644 --- a/app_pojavlauncher/src/main/AndroidManifest.xml +++ b/app_pojavlauncher/src/main/AndroidManifest.xml @@ -106,7 +106,7 @@ 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..00cc8e53be 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java @@ -156,7 +156,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 @@ -227,11 +232,6 @@ protected void onPause() { mInstallTracker.detach(); } - @Override - public boolean setFullscreen() { - return false; - } - @Override protected void onStart() { super.onStart(); 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..beb826c564 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -33,9 +33,6 @@ 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; @@ -47,6 +44,10 @@ 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; @@ -539,35 +540,32 @@ private static void setFullscreenLegacy(Activity activity, boolean 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); + 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) -> { + if (fullscreen && !activity.isInMultiWindowMode()) { + windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()); + activity.getWindow().setDecorFitsSystemWindows(false); + } else { + windowInsetsController.show(WindowInsetsCompat.Type.systemBars()); + activity.getWindow().setDecorFitsSystemWindows(true); + } + + return ViewCompat.onApplyWindowInsets(view, windowInsets); + }); + } public static void setFullscreen(Activity activity, boolean fullscreen) { 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/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/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(); From f1cb9e60cf5f5b75b019527adce295278107dd20 Mon Sep 17 00:00:00 2001 From: Mathias Boulay Date: Wed, 11 Dec 2024 19:41:35 +0100 Subject: [PATCH 08/34] chore(build): update gradle to 8.9 and AGP to 8.7.2 (#6353) * chore(build): update gradle to 8.9 and AGP to 8.7.2 This may or may not force the generation of all sub projects * build(actions): update GH Action gradle version * chore(build): update gradle to 8.11 * fix(build): allow proguard profile to launch It is still plagued with bugs, but it might stay handy the day we need to push some limits * Fix: allow CriticalNativeTest in proguard build * Build: Allow proguard granularity --- .github/workflows/android.yml | 4 +-- app_pojavlauncher/build.gradle | 14 +++++++-- .../kdt/pojavlaunch/CriticalNativeTest.java | 3 ++ .../main/java/net/kdt/pojavlaunch/Logger.java | 1 + .../net/kdt/pojavlaunch/MainActivity.java | 5 +++ .../java/org/lwjgl/glfw/CallbackBridge.java | 20 +++++++----- .../src/main/jni/input_bridge_v3.c | 1 + gradle.properties | 2 ++ gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++- gradlew | 29 ++++++++++-------- 11 files changed, 58 insertions(+), 25 deletions(-) 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/app_pojavlauncher/build.gradle b/app_pojavlauncher/build.gradle index 19bcb89309..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 { 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/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..92a25e9887 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; @@ -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/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/input_bridge_v3.c b/app_pojavlauncher/src/main/jni/input_bridge_v3.c index 4bd94f6e1a..03bb79f627 100644 --- a/app_pojavlauncher/src/main/jni/input_bridge_v3.c +++ b/app_pojavlauncher/src/main/jni/input_bridge_v3.c @@ -620,6 +620,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/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 ccebba7710deaf9f98673a68957ea02138b60d0a..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 61608 zcmb5VV{~QRw)Y#`wrv{~+qP{x72B%VwzFc}c2cp;N~)5ZbDrJayPv(!dGEd-##*zr z)#n-$y^sH|_dchh3@8{H5D*j;5D<{i*8l5IFJ|DjL!e)upfGNX(kojugZ3I`oH1PvW`wFW_ske0j@lB9bX zO;2)`y+|!@X(fZ1<2n!Qx*)_^Ai@Cv-dF&(vnudG?0CsddG_&Wtae(n|K59ew)6St z#dj7_(Cfwzh$H$5M!$UDd8=4>IQsD3xV=lXUq($;(h*$0^yd+b{qq63f0r_de#!o_ zXDngc>zy`uor)4A^2M#U*DC~i+dc<)Tb1Tv&~Ev@oM)5iJ4Sn#8iRw16XXuV50BS7 zdBL5Mefch(&^{luE{*5qtCZk$oFr3RH=H!c3wGR=HJ(yKc_re_X9pD` zJ;uxPzUfVpgU>DSq?J;I@a+10l0ONXPcDkiYcihREt5~T5Gb}sT0+6Q;AWHl`S5dV>lv%-p9l#xNNy7ZCr%cyqHY%TZ8Q4 zbp&#ov1*$#grNG#1vgfFOLJCaNG@K|2!W&HSh@3@Y%T?3YI75bJp!VP*$*!< z;(ffNS_;@RJ`=c7yX04!u3JP*<8jeqLHVJu#WV&v6wA!OYJS4h<_}^QI&97-;=ojW zQ-1t)7wnxG*5I%U4)9$wlv5Fr;cIizft@&N+32O%B{R1POm$oap@&f| zh+5J{>U6ftv|vAeKGc|zC=kO(+l7_cLpV}-D#oUltScw})N>~JOZLU_0{Ka2e1evz z{^a*ZrLr+JUj;)K&u2CoCAXLC2=fVScI(m_p~0FmF>>&3DHziouln?;sxW`NB}cSX z8?IsJB)Z=aYRz!X=yJn$kyOWK%rCYf-YarNqKzmWu$ZvkP12b4qH zhS9Q>j<}(*frr?z<%9hl*i^#@*O2q(Z^CN)c2c z>1B~D;@YpG?G!Yk+*yn4vM4sO-_!&m6+`k|3zd;8DJnxsBYtI;W3We+FN@|tQ5EW= z!VU>jtim0Mw#iaT8t_<+qKIEB-WwE04lBd%Letbml9N!?SLrEG$nmn7&W(W`VB@5S zaY=sEw2}i@F_1P4OtEw?xj4@D6>_e=m=797#hg}f*l^`AB|Y0# z9=)o|%TZFCY$SzgSjS|8AI-%J4x}J)!IMxY3_KYze`_I=c1nmrk@E8c9?MVRu)7+Ue79|)rBX7tVB7U|w4*h(;Gi3D9le49B38`wuv zp7{4X^p+K4*$@gU(Tq3K1a#3SmYhvI42)GzG4f|u zwQFT1n_=n|jpi=70-yE9LA+d*T8u z`=VmmXJ_f6WmZveZPct$Cgu^~gFiyL>Lnpj*6ee>*0pz=t$IJ}+rE zsf@>jlcG%Wx;Cp5x)YSVvB1$yyY1l&o zvwX=D7k)Dn;ciX?Z)Pn8$flC8#m`nB&(8?RSdBvr?>T9?E$U3uIX7T?$v4dWCa46 z+&`ot8ZTEgp7G+c52oHJ8nw5}a^dwb_l%MOh(ebVj9>_koQP^$2B~eUfSbw9RY$_< z&DDWf2LW;b0ZDOaZ&2^i^g+5uTd;GwO(-bbo|P^;CNL-%?9mRmxEw~5&z=X^Rvbo^WJW=n_%*7974RY}JhFv46> zd}`2|qkd;89l}R;i~9T)V-Q%K)O=yfVKNM4Gbacc7AOd>#^&W&)Xx!Uy5!BHnp9kh z`a(7MO6+Ren#>R^D0K)1sE{Bv>}s6Rb9MT14u!(NpZOe-?4V=>qZ>}uS)!y~;jEUK z&!U7Fj&{WdgU#L0%bM}SYXRtM5z!6M+kgaMKt%3FkjWYh=#QUpt$XX1!*XkpSq-pl zhMe{muh#knk{9_V3%qdDcWDv}v)m4t9 zQhv{;} zc{}#V^N3H>9mFM8`i`0p+fN@GqX+kl|M94$BK3J-X`Hyj8r!#x6Vt(PXjn?N)qedP z=o1T^#?1^a{;bZ&x`U{f?}TMo8ToN zkHj5v|}r}wDEi7I@)Gj+S1aE-GdnLN+$hw!=DzglMaj#{qjXi_dwpr|HL(gcCXwGLEmi|{4&4#OZ4ChceA zKVd4K!D>_N=_X;{poT~4Q+!Le+ZV>=H7v1*l%w`|`Dx8{)McN@NDlQyln&N3@bFpV z_1w~O4EH3fF@IzJ9kDk@7@QctFq8FbkbaH7K$iX=bV~o#gfh?2JD6lZf(XP>~DACF)fGFt)X%-h1yY~MJU{nA5 ze2zxWMs{YdX3q5XU*9hOH0!_S24DOBA5usB+Ws$6{|AMe*joJ?RxfV}*7AKN9V*~J zK+OMcE@bTD>TG1*yc?*qGqjBN8mgg@h1cJLDv)0!WRPIkC` zZrWXrceVw;fB%3`6kq=a!pq|hFIsQ%ZSlo~)D z|64!aCnw-?>}AG|*iOl44KVf8@|joXi&|)1rB;EQWgm+iHfVbgllP$f!$Wf42%NO5b(j9Bw6L z;0dpUUK$5GX4QbMlTmLM_jJt!ur`_0~$b#BB7FL*%XFf<b__1o)Ao3rlobbN8-(T!1d-bR8D3S0@d zLI!*GMb5s~Q<&sjd}lBb8Nr0>PqE6_!3!2d(KAWFxa{hm`@u|a(%#i(#f8{BP2wbs zt+N_slWF4IF_O|{w`c~)Xvh&R{Au~CFmW#0+}MBd2~X}t9lz6*E7uAD`@EBDe$>7W zzPUkJx<`f$0VA$=>R57^(K^h86>09?>_@M(R4q($!Ck6GG@pnu-x*exAx1jOv|>KH zjNfG5pwm`E-=ydcb+3BJwuU;V&OS=6yM^4Jq{%AVqnTTLwV`AorIDD}T&jWr8pB&j28fVtk_y*JRP^t@l*($UZ z6(B^-PBNZ+z!p?+e8@$&jCv^EWLb$WO=}Scr$6SM*&~B95El~;W_0(Bvoha|uQ1T< zO$%_oLAwf1bW*rKWmlD+@CP&$ObiDy=nh1b2ejz%LO9937N{LDe7gle4i!{}I$;&Y zkexJ9Ybr+lrCmKWg&}p=`2&Gf10orS?4$VrzWidT=*6{KzOGMo?KI0>GL0{iFWc;C z+LPq%VH5g}6V@-tg2m{C!-$fapJ9y}c$U}aUmS{9#0CM*8pC|sfer!)nG7Ji>mfRh z+~6CxNb>6eWKMHBz-w2{mLLwdA7dA-qfTu^A2yG1+9s5k zcF=le_UPYG&q!t5Zd_*E_P3Cf5T6821bO`daa`;DODm8Ih8k89=RN;-asHIigj`n=ux>*f!OC5#;X5i;Q z+V!GUy0|&Y_*8k_QRUA8$lHP;GJ3UUD08P|ALknng|YY13)}!!HW@0z$q+kCH%xet zlWf@BXQ=b=4}QO5eNnN~CzWBbHGUivG=`&eWK}beuV*;?zt=P#pM*eTuy3 zP}c#}AXJ0OIaqXji78l;YrP4sQe#^pOqwZUiiN6^0RCd#D271XCbEKpk`HI0IsN^s zES7YtU#7=8gTn#lkrc~6)R9u&SX6*Jk4GFX7){E)WE?pT8a-%6P+zS6o&A#ml{$WX zABFz#i7`DDlo{34)oo?bOa4Z_lNH>n;f0nbt$JfAl~;4QY@}NH!X|A$KgMmEsd^&Y zt;pi=>AID7ROQfr;MsMtClr5b0)xo|fwhc=qk33wQ|}$@?{}qXcmECh>#kUQ-If0$ zseb{Wf4VFGLNc*Rax#P8ko*=`MwaR-DQ8L8V8r=2N{Gaips2_^cS|oC$+yScRo*uF zUO|5=?Q?{p$inDpx*t#Xyo6=s?bbN}y>NNVxj9NZCdtwRI70jxvm3!5R7yiWjREEd zDUjrsZhS|P&|Ng5r+f^kA6BNN#|Se}_GF>P6sy^e8kBrgMv3#vk%m}9PCwUWJg-AD zFnZ=}lbi*mN-AOm zCs)r=*YQAA!`e#1N>aHF=bb*z*hXH#Wl$z^o}x##ZrUc=kh%OHWhp=7;?8%Xj||@V?1c ziWoaC$^&04;A|T)!Zd9sUzE&$ODyJaBpvqsw19Uiuq{i#VK1!htkdRWBnb z`{rat=nHArT%^R>u#CjjCkw-7%g53|&7z-;X+ewb?OLWiV|#nuc8mp*LuGSi3IP<<*Wyo9GKV7l0Noa4Jr0g3p_$ z*R9{qn=?IXC#WU>48-k5V2Oc_>P;4_)J@bo1|pf=%Rcbgk=5m)CJZ`caHBTm3%!Z9 z_?7LHr_BXbKKr=JD!%?KhwdYSdu8XxPoA{n8^%_lh5cjRHuCY9Zlpz8g+$f@bw@0V z+6DRMT9c|>1^3D|$Vzc(C?M~iZurGH2pXPT%F!JSaAMdO%!5o0uc&iqHx?ImcX6fI zCApkzc~OOnfzAd_+-DcMp&AOQxE_EsMqKM{%dRMI5`5CT&%mQO?-@F6tE*xL?aEGZ z8^wH@wRl`Izx4sDmU>}Ym{ybUm@F83qqZPD6nFm?t?(7>h*?`fw)L3t*l%*iw0Qu#?$5eq!Qc zpQvqgSxrd83NsdO@lL6#{%lsYXWen~d3p4fGBb7&5xqNYJ)yn84!e1PmPo7ChVd%4 zHUsV0Mh?VpzZD=A6%)Qrd~i7 z96*RPbid;BN{Wh?adeD_p8YU``kOrGkNox3D9~!K?w>#kFz!4lzOWR}puS(DmfjJD z`x0z|qB33*^0mZdM&6$|+T>fq>M%yoy(BEjuh9L0>{P&XJ3enGpoQRx`v6$txXt#c z0#N?b5%srj(4xmPvJxrlF3H%OMB!jvfy z;wx8RzU~lb?h_}@V=bh6p8PSb-dG|-T#A?`c&H2`_!u+uenIZe`6f~A7r)`9m8atC zt(b|6Eg#!Q*DfRU=Ix`#B_dK)nnJ_+>Q<1d7W)eynaVn`FNuN~%B;uO2}vXr5^zi2 z!ifIF5@Zlo0^h~8+ixFBGqtweFc`C~JkSq}&*a3C}L?b5Mh-bW=e)({F_g4O3 zb@SFTK3VD9QuFgFnK4Ve_pXc3{S$=+Z;;4+;*{H}Rc;845rP?DLK6G5Y-xdUKkA6E3Dz&5f{F^FjJQ(NSpZ8q-_!L3LL@H* zxbDF{gd^U3uD;)a)sJwAVi}7@%pRM&?5IaUH%+m{E)DlA_$IA1=&jr{KrhD5q&lTC zAa3c)A(K!{#nOvenH6XrR-y>*4M#DpTTOGQEO5Jr6kni9pDW`rvY*fs|ItV;CVITh z=`rxcH2nEJpkQ^(;1c^hfb8vGN;{{oR=qNyKtR1;J>CByul*+=`NydWnSWJR#I2lN zTvgnR|MBx*XFsfdA&;tr^dYaqRZp*2NwkAZE6kV@1f{76e56eUmGrZ>MDId)oqSWw z7d&r3qfazg+W2?bT}F)4jD6sWaw`_fXZGY&wnGm$FRPFL$HzVTH^MYBHWGCOk-89y zA+n+Q6EVSSCpgC~%uHfvyg@ufE^#u?JH?<73A}jj5iILz4Qqk5$+^U(SX(-qv5agK znUkfpke(KDn~dU0>gdKqjTkVk`0`9^0n_wzXO7R!0Thd@S;U`y)VVP&mOd-2 z(hT(|$=>4FY;CBY9#_lB$;|Wd$aOMT5O_3}DYXEHn&Jrc3`2JiB`b6X@EUOD zVl0S{ijm65@n^19T3l%>*;F(?3r3s?zY{thc4%AD30CeL_4{8x6&cN}zN3fE+x<9; zt2j1RRVy5j22-8U8a6$pyT+<`f+x2l$fd_{qEp_bfxfzu>ORJsXaJn4>U6oNJ#|~p z`*ZC&NPXl&=vq2{Ne79AkQncuxvbOG+28*2wU$R=GOmns3W@HE%^r)Fu%Utj=r9t` zd;SVOnA(=MXgnOzI2@3SGKHz8HN~Vpx&!Ea+Df~`*n@8O=0!b4m?7cE^K*~@fqv9q zF*uk#1@6Re_<^9eElgJD!nTA@K9C732tV~;B`hzZ321Ph=^BH?zXddiu{Du5*IPg} zqDM=QxjT!Rp|#Bkp$(mL)aar)f(dOAXUiw81pX0DC|Y4;>Vz>>DMshoips^8Frdv} zlTD=cKa48M>dR<>(YlLPOW%rokJZNF2gp8fwc8b2sN+i6&-pHr?$rj|uFgktK@jg~ zIFS(%=r|QJ=$kvm_~@n=ai1lA{7Z}i+zj&yzY+!t$iGUy|9jH#&oTNJ;JW-3n>DF+ z3aCOzqn|$X-Olu_p7brzn`uk1F*N4@=b=m;S_C?#hy{&NE#3HkATrg?enaVGT^$qIjvgc61y!T$9<1B@?_ibtDZ{G zeXInVr5?OD_nS_O|CK3|RzzMmu+8!#Zb8Ik;rkIAR%6?$pN@d<0dKD2c@k2quB%s( zQL^<_EM6ow8F6^wJN1QcPOm|ehA+dP(!>IX=Euz5qqIq}Y3;ibQtJnkDmZ8c8=Cf3 zu`mJ!Q6wI7EblC5RvP*@)j?}W=WxwCvF3*5Up_`3*a~z$`wHwCy)2risye=1mSp%p zu+tD6NAK3o@)4VBsM!@);qgsjgB$kkCZhaimHg&+k69~drbvRTacWKH;YCK(!rC?8 zP#cK5JPHSw;V;{Yji=55X~S+)%(8fuz}O>*F3)hR;STU`z6T1aM#Wd+FP(M5*@T1P z^06O;I20Sk!bxW<-O;E081KRdHZrtsGJflFRRFS zdi5w9OVDGSL3 zNrC7GVsGN=b;YH9jp8Z2$^!K@h=r-xV(aEH@#JicPy;A0k1>g1g^XeR`YV2HfmqXY zYbRwaxHvf}OlCAwHoVI&QBLr5R|THf?nAevV-=~V8;gCsX>jndvNOcFA+DI+zbh~# zZ7`qNk&w+_+Yp!}j;OYxIfx_{f0-ONc?mHCiCUak=>j>~>YR4#w# zuKz~UhT!L~GfW^CPqG8Lg)&Rc6y^{%3H7iLa%^l}cw_8UuG;8nn9)kbPGXS}p3!L_ zd#9~5CrH8xtUd?{d2y^PJg+z(xIfRU;`}^=OlehGN2=?}9yH$4Rag}*+AWotyxfCJ zHx=r7ZH>j2kV?%7WTtp+-HMa0)_*DBBmC{sd$)np&GEJ__kEd`xB5a2A z*J+yx>4o#ZxwA{;NjhU*1KT~=ZK~GAA;KZHDyBNTaWQ1+;tOFFthnD)DrCn`DjBZ% zk$N5B4^$`n^jNSOr=t(zi8TN4fpaccsb`zOPD~iY=UEK$0Y70bG{idLx@IL)7^(pL z{??Bnu=lDeguDrd%qW1)H)H`9otsOL-f4bSu};o9OXybo6J!Lek`a4ff>*O)BDT_g z<6@SrI|C9klY(>_PfA^qai7A_)VNE4c^ZjFcE$Isp>`e5fLc)rg@8Q_d^Uk24$2bn z9#}6kZ2ZxS9sI(RqT7?El2@B+($>eBQrNi_k#CDJ8D9}8$mmm z4oSKO^F$i+NG)-HE$O6s1--6EzJa?C{x=QgK&c=)b(Q9OVoAXYEEH20G|q$}Hue%~ zO3B^bF=t7t48sN zWh_zA`w~|){-!^g?6Mqf6ieV zFx~aPUOJGR=4{KsW7I?<=J2|lY`NTU=lt=%JE9H1vBpkcn=uq(q~=?iBt_-r(PLBM zP-0dxljJO>4Wq-;stY)CLB4q`-r*T$!K2o}?E-w_i>3_aEbA^MB7P5piwt1dI-6o!qWCy0 ztYy!x9arGTS?kabkkyv*yxvsPQ7Vx)twkS6z2T@kZ|kb8yjm+^$|sEBmvACeqbz)RmxkkDQX-A*K!YFziuhwb|ym>C$}U|J)4y z$(z#)GH%uV6{ec%Zy~AhK|+GtG8u@c884Nq%w`O^wv2#A(&xH@c5M`Vjk*SR_tJnq z0trB#aY)!EKW_}{#L3lph5ow=@|D5LzJYUFD6 z7XnUeo_V0DVSIKMFD_T0AqAO|#VFDc7c?c-Q%#u00F%!_TW1@JVnsfvm@_9HKWflBOUD~)RL``-!P;(bCON_4eVdduMO>?IrQ__*zE@7(OX zUtfH@AX*53&xJW*Pu9zcqxGiM>xol0I~QL5B%Toog3Jlenc^WbVgeBvV8C8AX^Vj& z^I}H})B=VboO%q1;aU5ACMh{yK4J;xlMc`jCnZR^!~LDs_MP&8;dd@4LDWw~*>#OT zeZHwdQWS!tt5MJQI~cw|Ka^b4c|qyd_ly(+Ql2m&AAw^ zQeSXDOOH!!mAgzAp0z)DD>6Xo``b6QwzUV@w%h}Yo>)a|xRi$jGuHQhJVA%>)PUvK zBQ!l0hq<3VZ*RnrDODP)>&iS^wf64C;MGqDvx>|p;35%6(u+IHoNbK z;Gb;TneFo*`zUKS6kwF*&b!U8e5m4YAo03a_e^!5BP42+r)LFhEy?_7U1IR<; z^0v|DhCYMSj<-;MtY%R@Fg;9Kky^pz_t2nJfKWfh5Eu@_l{^ph%1z{jkg5jQrkvD< z#vdK!nku*RrH~TdN~`wDs;d>XY1PH?O<4^U4lmA|wUW{Crrv#r%N>7k#{Gc44Fr|t z@UZP}Y-TrAmnEZ39A*@6;ccsR>)$A)S>$-Cj!=x$rz7IvjHIPM(TB+JFf{ehuIvY$ zsDAwREg*%|=>Hw$`us~RP&3{QJg%}RjJKS^mC_!U;E5u>`X`jW$}P`Mf}?7G7FX#{ zE(9u1SO;3q@ZhDL9O({-RD+SqqPX)`0l5IQu4q)49TUTkxR(czeT}4`WV~pV*KY&i zAl3~X%D2cPVD^B43*~&f%+Op)wl<&|D{;=SZwImydWL6@_RJjxP2g)s=dH)u9Npki zs~z9A+3fj0l?yu4N0^4aC5x)Osnm0qrhz@?nwG_`h(71P znbIewljU%T*cC=~NJy|)#hT+lx#^5MuDDnkaMb*Efw9eThXo|*WOQzJ*#3dmRWm@! zfuSc@#kY{Um^gBc^_Xdxnl!n&y&}R4yAbK&RMc+P^Ti;YIUh|C+K1|=Z^{nZ}}rxH*v{xR!i%qO~o zTr`WDE@k$M9o0r4YUFFeQO7xCu_Zgy)==;fCJ94M_rLAv&~NhfvcLWCoaGg2ao~3e zBG?Ms9B+efMkp}7BhmISGWmJsKI@a8b}4lLI48oWKY|8?zuuNc$lt5Npr+p7a#sWu zh!@2nnLBVJK!$S~>r2-pN||^w|fY`CT{TFnJy`B|e5;=+_v4l8O-fkN&UQbA4NKTyntd zqK{xEKh}U{NHoQUf!M=2(&w+eef77VtYr;xs%^cPfKLObyOV_9q<(%76-J%vR>w9!us-0c-~Y?_EVS%v!* z15s2s3eTs$Osz$JayyH|5nPAIPEX=U;r&p;K14G<1)bvn@?bM5kC{am|C5%hyxv}a z(DeSKI5ZfZ1*%dl8frIX2?);R^^~LuDOpNpk-2R8U1w92HmG1m&|j&J{EK=|p$;f9 z7Rs5|jr4r8k5El&qcuM+YRlKny%t+1CgqEWO>3;BSRZi(LA3U%Jm{@{y+A+w(gzA< z7dBq6a1sEWa4cD0W7=Ld9z0H7RI^Z7vl(bfA;72j?SWCo`#5mVC$l1Q2--%V)-uN* z9ha*s-AdfbDZ8R8*fpwjzx=WvOtmSzGFjC#X)hD%Caeo^OWjS(3h|d9_*U)l%{Ab8 zfv$yoP{OuUl@$(-sEVNt{*=qi5P=lpxWVuz2?I7Dc%BRc+NGNw+323^ z5BXGfS71oP^%apUo(Y#xkxE)y?>BFzEBZ}UBbr~R4$%b7h3iZu3S(|A;&HqBR{nK& z$;GApNnz=kNO^FL&nYcfpB7Qg;hGJPsCW44CbkG1@l9pn0`~oKy5S777uH)l{irK!ru|X+;4&0D;VE*Ii|<3P zUx#xUqvZT5kVQxsF#~MwKnv7;1pR^0;PW@$@T7I?s`_rD1EGUdSA5Q(C<>5SzE!vw z;{L&kKFM-MO>hy#-8z`sdVx})^(Dc-dw;k-h*9O2_YZw}|9^y-|8RQ`BWJUJL(Cer zP5Z@fNc>pTXABbTRY-B5*MphpZv6#i802giwV&SkFCR zGMETyUm(KJbh+&$8X*RB#+{surjr;8^REEt`2&Dubw3$mx>|~B5IKZJ`s_6fw zKAZx9&PwBqW1Oz0r0A4GtnZd7XTKViX2%kPfv+^X3|_}RrQ2e3l=KG_VyY`H?I5&CS+lAX5HbA%TD9u6&s#v!G> zzW9n4J%d5ye7x0y`*{KZvqyXUfMEE^ZIffzI=Hh|3J}^yx7eL=s+TPH(Q2GT-sJ~3 zI463C{(ag7-hS1ETtU;_&+49ABt5!A7CwLwe z=SoA8mYZIQeU;9txI=zcQVbuO%q@E)JI+6Q!3lMc=Gbj(ASg-{V27u>z2e8n;Nc*pf}AqKz1D>p9G#QA+7mqqrEjGfw+85Uyh!=tTFTv3|O z+)-kFe_8FF_EkTw!YzwK^Hi^_dV5x-Ob*UWmD-})qKj9@aE8g240nUh=g|j28^?v7 zHRTBo{0KGaWBbyX2+lx$wgXW{3aUab6Bhm1G1{jTC7ota*JM6t+qy)c5<@ zpc&(jVdTJf(q3xB=JotgF$X>cxh7k*(T`-V~AR+`%e?YOeALQ2Qud( zz35YizXt(aW3qndR}fTw1p()Ol4t!D1pitGNL95{SX4ywzh0SF;=!wf=?Q?_h6!f* zh7<+GFi)q|XBsvXZ^qVCY$LUa{5?!CgwY?EG;*)0ceFe&=A;!~o`ae}Z+6me#^sv- z1F6=WNd6>M(~ z+092z>?Clrcp)lYNQl9jN-JF6n&Y0mp7|I0dpPx+4*RRK+VQI~>en0Dc;Zfl+x z_e_b7s`t1_A`RP3$H}y7F9_na%D7EM+**G_Z0l_nwE+&d_kc35n$Fxkd4r=ltRZhh zr9zER8>j(EdV&Jgh(+i}ltESBK62m0nGH6tCBr90!4)-`HeBmz54p~QP#dsu%nb~W z7sS|(Iydi>C@6ZM(Us!jyIiszMkd)^u<1D+R@~O>HqZIW&kearPWmT>63%_t2B{_G zX{&a(gOYJx!Hq=!T$RZ&<8LDnxsmx9+TBL0gTk$|vz9O5GkK_Yx+55^R=2g!K}NJ3 zW?C;XQCHZl7H`K5^BF!Q5X2^Mj93&0l_O3Ea3!Ave|ixx+~bS@Iv18v2ctpSt4zO{ zp#7pj!AtDmti$T`e9{s^jf(ku&E|83JIJO5Qo9weT6g?@vX!{7)cNwymo1+u(YQ94 zopuz-L@|5=h8A!(g-MXgLJC0MA|CgQF8qlonnu#j z;uCeq9ny9QSD|p)9sp3ebgY3rk#y0DA(SHdh$DUm^?GI<>%e1?&}w(b zdip1;P2Z=1wM+$q=TgLP$}svd!vk+BZ@h<^4R=GS2+sri7Z*2f`9 z5_?i)xj?m#pSVchk-SR!2&uNhzEi+#5t1Z$o0PoLGz*pT64%+|Wa+rd5Z}60(j?X= z{NLjtgRb|W?CUADqOS@(*MA-l|E342NxRaxLTDqsOyfWWe%N(jjBh}G zm7WPel6jXijaTiNita+z(5GCO0NM=Melxud57PP^d_U## zbA;9iVi<@wr0DGB8=T9Ab#2K_#zi=$igyK48@;V|W`fg~7;+!q8)aCOo{HA@vpSy-4`^!ze6-~8|QE||hC{ICKllG9fbg_Y7v z$jn{00!ob3!@~-Z%!rSZ0JO#@>|3k10mLK0JRKP-Cc8UYFu>z93=Ab-r^oL2 zl`-&VBh#=-?{l1TatC;VweM^=M7-DUE>m+xO7Xi6vTEsReyLs8KJ+2GZ&rxw$d4IT zPXy6pu^4#e;;ZTsgmG+ZPx>piodegkx2n0}SM77+Y*j^~ICvp#2wj^BuqRY*&cjmL zcKp78aZt>e{3YBb4!J_2|K~A`lN=u&5j!byw`1itV(+Q_?RvV7&Z5XS1HF)L2v6ji z&kOEPmv+k_lSXb{$)of~(BkO^py&7oOzpjdG>vI1kcm_oPFHy38%D4&A4h_CSo#lX z2#oqMCTEP7UvUR3mwkPxbl8AMW(e{ARi@HCYLPSHE^L<1I}OgZD{I#YH#GKnpRmW3 z2jkz~Sa(D)f?V?$gNi?6)Y;Sm{&?~2p=0&BUl_(@hYeX8YjaRO=IqO7neK0RsSNdYjD zaw$g2sG(>JR=8Iz1SK4`*kqd_3-?;_BIcaaMd^}<@MYbYisWZm2C2|Np_l|8r9yM|JkUngSo@?wci(7&O9a z%|V(4C1c9pps0xxzPbXH=}QTxc2rr7fXk$9`a6TbWKPCz&p=VsB8^W96W=BsB|7bc zf(QR8&Ktj*iz)wK&mW`#V%4XTM&jWNnDF56O+2bo<3|NyUhQ%#OZE8$Uv2a@J>D%t zMVMiHh?es!Ex19q&6eC&L=XDU_BA&uR^^w>fpz2_`U87q_?N2y;!Z!bjoeKrzfC)} z?m^PM=(z{%n9K`p|7Bz$LuC7!>tFOuN74MFELm}OD9?%jpT>38J;=1Y-VWtZAscaI z_8jUZ#GwWz{JqvGEUmL?G#l5E=*m>`cY?m*XOc*yOCNtpuIGD+Z|kn4Xww=BLrNYS zGO=wQh}Gtr|7DGXLF%|`G>J~l{k^*{;S-Zhq|&HO7rC_r;o`gTB7)uMZ|WWIn@e0( zX$MccUMv3ABg^$%_lNrgU{EVi8O^UyGHPNRt%R!1#MQJn41aD|_93NsBQhP80yP<9 zG4(&0u7AtJJXLPcqzjv`S~5;Q|5TVGccN=Uzm}K{v)?f7W!230C<``9(64}D2raRU zAW5bp%}VEo{4Rko`bD%Ehf=0voW?-4Mk#d3_pXTF!-TyIt6U+({6OXWVAa;s-`Ta5 zTqx&8msH3+DLrVmQOTBOAj=uoxKYT3DS1^zBXM?1W+7gI!aQNPYfUl{3;PzS9*F7g zWJN8x?KjBDx^V&6iCY8o_gslO16=kh(|Gp)kz8qlQ`dzxQv;)V&t+B}wwdi~uBs4? zu~G|}y!`3;8#vIMUdyC7YEx6bb^1o}G!Jky4cN?BV9ejBfN<&!4M)L&lRKiuMS#3} z_B}Nkv+zzxhy{dYCW$oGC&J(Ty&7%=5B$sD0bkuPmj7g>|962`(Q{ZZMDv%YMuT^KweiRDvYTEop3IgFv#)(w>1 zSzH>J`q!LK)c(AK>&Ib)A{g`Fdykxqd`Yq@yB}E{gnQV$K!}RsgMGWqC3DKE(=!{}ekB3+(1?g}xF>^icEJbc z5bdxAPkW90atZT+&*7qoLqL#p=>t-(-lsnl2XMpZcYeW|o|a322&)yO_8p(&Sw{|b zn(tY$xn5yS$DD)UYS%sP?c|z>1dp!QUD)l;aW#`%qMtQJjE!s2z`+bTSZmLK7SvCR z=@I4|U^sCwZLQSfd*ACw9B@`1c1|&i^W_OD(570SDLK`MD0wTiR8|$7+%{cF&){$G zU~|$^Ed?TIxyw{1$e|D$050n8AjJvvOWhLtLHbSB|HIfjMp+gu>DraHZJRrdO53(= z+o-f{+qNog+qSLB%KY;5>Av6X(>-qYk3IIEwZ5~6a+P9lMpC^ z8CJ0q>rEpjlsxCvJm=kms@tlN4+sv}He`xkr`S}bGih4t`+#VEIt{1veE z{ZLtb_pSbcfcYPf4=T1+|BtR!x5|X#x2TZEEkUB6kslKAE;x)*0x~ES0kl4Dex4e- zT2P~|lT^vUnMp{7e4OExfxak0EE$Hcw;D$ehTV4a6hqxru0$|Mo``>*a5=1Ym0u>BDJKO|=TEWJ5jZu!W}t$Kv{1!q`4Sn7 zrxRQOt>^6}Iz@%gA3&=5r;Lp=N@WKW;>O!eGIj#J;&>+3va^~GXRHCY2}*g#9ULab zitCJt-OV0*D_Q3Q`p1_+GbPxRtV_T`jyATjax<;zZ?;S+VD}a(aN7j?4<~>BkHK7bO8_Vqfdq1#W&p~2H z&w-gJB4?;Q&pG9%8P(oOGZ#`!m>qAeE)SeL*t8KL|1oe;#+uOK6w&PqSDhw^9-&Fa zuEzbi!!7|YhlWhqmiUm!muO(F8-F7|r#5lU8d0+=;<`{$mS=AnAo4Zb^{%p}*gZL! zeE!#-zg0FWsSnablw!9$<&K(#z!XOW z;*BVx2_+H#`1b@>RtY@=KqD)63brP+`Cm$L1@ArAddNS1oP8UE$p05R=bvZoYz+^6 z<)!v7pRvi!u_-V?!d}XWQR1~0q(H3{d^4JGa=W#^Z<@TvI6J*lk!A zZ*UIKj*hyO#5akL*Bx6iPKvR3_2-^2mw|Rh-3O_SGN3V9GRo52Q;JnW{iTGqb9W99 z7_+F(Op6>~3P-?Q8LTZ-lwB}xh*@J2Ni5HhUI3`ct|*W#pqb>8i*TXOLn~GlYECIj zhLaa_rBH|1jgi(S%~31Xm{NB!30*mcsF_wgOY2N0XjG_`kFB+uQuJbBm3bIM$qhUyE&$_u$gb zpK_r{99svp3N3p4yHHS=#csK@j9ql*>j0X=+cD2dj<^Wiu@i>c_v zK|ovi7}@4sVB#bzq$n3`EgI?~xDmkCW=2&^tD5RuaSNHf@Y!5C(Is$hd6cuyoK|;d zO}w2AqJPS`Zq+(mc*^%6qe>1d&(n&~()6-ZATASNPsJ|XnxelLkz8r1x@c2XS)R*H(_B=IN>JeQUR;T=i3<^~;$<+8W*eRKWGt7c#>N`@;#!`kZ!P!&{9J1>_g8Zj zXEXxmA=^{8A|3=Au+LfxIWra)4p<}1LYd_$1KI0r3o~s1N(x#QYgvL4#2{z8`=mXy zQD#iJ0itk1d@Iy*DtXw)Wz!H@G2St?QZFz zVPkM%H8Cd2EZS?teQN*Ecnu|PrC!a7F_XX}AzfZl3fXfhBtc2-)zaC2eKx*{XdM~QUo4IwcGgVdW69 z1UrSAqqMALf^2|(I}hgo38l|Ur=-SC*^Bo5ej`hb;C$@3%NFxx5{cxXUMnTyaX{>~ zjL~xm;*`d08bG_K3-E+TI>#oqIN2=An(C6aJ*MrKlxj?-;G zICL$hi>`F%{xd%V{$NhisHSL~R>f!F7AWR&7b~TgLu6!3s#~8|VKIX)KtqTH5aZ8j zY?wY)XH~1_a3&>#j7N}0az+HZ;is;Zw(Am{MX}YhDTe(t{ZZ;TG}2qWYO+hdX}vp9 z@uIRR8g#y~-^E`Qyem(31{H0&V?GLdq9LEOb2(ea#e-$_`5Q{T%E?W(6 z(XbX*Ck%TQM;9V2LL}*Tf`yzai{0@pYMwBu%(I@wTY!;kMrzcfq0w?X`+y@0ah510 zQX5SU(I!*Fag4U6a7Lw%LL;L*PQ}2v2WwYF(lHx_Uz2ceI$mnZ7*eZ?RFO8UvKI0H z9Pq-mB`mEqn6n_W9(s~Jt_D~j!Ln9HA)P;owD-l~9FYszs)oEKShF9Zzcmnb8kZ7% zQ`>}ki1kwUO3j~ zEmh140sOkA9v>j@#56ymn_RnSF`p@9cO1XkQy6_Kog?0ivZDb`QWOX@tjMd@^Qr(p z!sFN=A)QZm!sTh(#q%O{Ovl{IxkF!&+A)w2@50=?a-+VuZt6On1;d4YtUDW{YNDN_ zG@_jZi1IlW8cck{uHg^g=H58lPQ^HwnybWy@@8iw%G! zwB9qVGt_?~M*nFAKd|{cGg+8`+w{j_^;nD>IrPf-S%YjBslSEDxgKH{5p)3LNr!lD z4ii)^%d&cCXIU7UK?^ZQwmD(RCd=?OxmY(Ko#+#CsTLT;p#A%{;t5YpHFWgl+@)N1 zZ5VDyB;+TN+g@u~{UrWrv)&#u~k$S&GeW)G{M#&Di)LdYk?{($Cq zZGMKeYW)aMtjmKgvF0Tg>Mmkf9IB#2tYmH-s%D_9y3{tfFmX1BSMtbe<(yqAyWX60 zzkgSgKb3c{QPG2MalYp`7mIrYg|Y<4Jk?XvJK)?|Ecr+)oNf}XLPuTZK%W>;<|r+% zTNViRI|{sf1v7CsWHvFrkQ$F7+FbqPQ#Bj7XX=#M(a~9^80}~l-DueX#;b}Ajn3VE z{BWI}$q{XcQ3g{(p>IOzFcAMDG0xL)H%wA)<(gl3I-oVhK~u_m=hAr&oeo|4lZbf} z+pe)c34Am<=z@5!2;_lwya;l?xV5&kWe}*5uBvckm(d|7R>&(iJNa6Y05SvlZcWBlE{{%2- z`86)Y5?H!**?{QbzGG~|k2O%eA8q=gxx-3}&Csf6<9BsiXC)T;x4YmbBIkNf;0Nd5 z%whM^!K+9zH>on_<&>Ws?^v-EyNE)}4g$Fk?Z#748e+GFp)QrQQETx@u6(1fk2!(W zWiCF~MomG*y4@Zk;h#2H8S@&@xwBIs|82R*^K(i*0MTE%Rz4rgO&$R zo9Neb;}_ulaCcdn3i17MO3NxzyJ=l;LU*N9ztBJ30j=+?6>N4{9YXg$m=^9@Cl9VY zbo^{yS@gU=)EpQ#;UIQBpf&zfCA;00H-ee=1+TRw@(h%W=)7WYSb5a%$UqNS@oI@= zDrq|+Y9e&SmZrH^iA>Of8(9~Cf-G(P^5Xb%dDgMMIl8gk6zdyh`D3OGNVV4P9X|EvIhplXDld8d z^YWtYUz@tpg*38Xys2?zj$F8%ivA47cGSl;hjD23#*62w3+fwxNE7M7zVK?x_`dBSgPK zWY_~wF~OEZi9|~CSH8}Xi>#8G73!QLCAh58W+KMJJC81{60?&~BM_0t-u|VsPBxn* zW7viEKwBBTsn_A{g@1!wnJ8@&h&d>!qAe+j_$$Vk;OJq`hrjzEE8Wjtm)Z>h=*M25 zOgETOM9-8xuuZ&^@rLObtcz>%iWe%!uGV09nUZ*nxJAY%&KAYGY}U1WChFik7HIw% zZP$3Bx|TG_`~19XV7kfi2GaBEhKap&)Q<9`aPs#^!kMjtPb|+-fX66z3^E)iwyXK7 z8)_p<)O{|i&!qxtgBvWXx8*69WO$5zACl++1qa;)0zlXf`eKWl!0zV&I`8?sG)OD2Vy?reNN<{eK+_ za4M;Hh%&IszR%)&gpgRCP}yheQ+l#AS-GnY81M!kzhWxIR?PW`G3G?} z$d%J28uQIuK@QxzGMKU_;r8P0+oIjM+k)&lZ39i#(ntY)*B$fdJnQ3Hw3Lsi8z&V+ zZly2}(Uzpt2aOubRjttzqrvinBFH4jrN)f0hy)tj4__UTwN)#1fj3-&dC_Vh7}ri* zfJ=oqLMJ-_<#rwVyN}_a-rFBe2>U;;1(7UKH!$L??zTbbzP#bvyg7OQBGQklJ~DgP zd<1?RJ<}8lWwSL)`jM53iG+}y2`_yUvC!JkMpbZyb&50V3sR~u+lok zT0uFRS-yx@8q4fPRZ%KIpLp8R#;2%c&Ra4p(GWRT4)qLaPNxa&?8!LRVdOUZ)2vrh zBSx&kB%#Y4!+>~)<&c>D$O}!$o{<1AB$M7-^`h!eW;c(3J~ztoOgy6Ek8Pwu5Y`Xion zFl9fb!k2`3uHPAbd(D^IZmwR5d8D$495nN2`Ue&`W;M-nlb8T-OVKt|fHk zBpjX$a(IR6*-swdNk@#}G?k6F-~c{AE0EWoZ?H|ZpkBxqU<0NUtvubJtwJ1mHV%9v?GdDw; zAyXZiD}f0Zdt-cl9(P1la+vQ$Er0~v}gYJVwQazv zH#+Z%2CIfOf90fNMGos|{zf&N`c0@x0N`tkFv|_9af3~<0z@mnf*e;%r*Fbuwl-IW z{}B3=(mJ#iwLIPiUP`J3SoP~#)6v;aRXJ)A-pD2?_2_CZ#}SAZ<#v7&Vk6{*i(~|5 z9v^nC`T6o`CN*n%&9+bopj^r|E(|pul;|q6m7Tx+U|UMjWK8o-lBSgc3ZF=rP{|l9 zc&R$4+-UG6i}c==!;I#8aDIbAvgLuB66CQLRoTMu~jdw`fPlKy@AKYWS-xyZzPg&JRAa@m-H43*+ne!8B7)HkQY4 zIh}NL4Q79a-`x;I_^>s$Z4J4-Ngq=XNWQ>yAUCoe&SMAYowP>r_O}S=V+3=3&(O=h zNJDYNs*R3Y{WLmBHc?mFEeA4`0Y`_CN%?8qbDvG2m}kMAiqCv`_BK z_6a@n`$#w6Csr@e2YsMx8udNWtNt=kcqDZdWZ-lGA$?1PA*f4?X*)hjn{sSo8!bHz zb&lGdAgBx@iTNPK#T_wy`KvOIZvTWqSHb=gWUCKXAiB5ckQI`1KkPx{{%1R*F2)Oc z(9p@yG{fRSWE*M9cdbrO^)8vQ2U`H6M>V$gK*rz!&f%@3t*d-r3mSW>D;wYxOhUul zk~~&ip5B$mZ~-F1orsq<|1bc3Zpw6)Ws5;4)HilsN;1tx;N6)tuePw& z==OlmaN*ybM&-V`yt|;vDz(_+UZ0m&&9#{9O|?0I|4j1YCMW;fXm}YT$0%EZ5^YEI z4i9WV*JBmEU{qz5O{#bs`R1wU%W$qKx?bC|e-iS&d*Qm7S=l~bMT{~m3iZl+PIXq{ zn-c~|l)*|NWLM%ysfTV-oR0AJ3O>=uB-vpld{V|cWFhI~sx>ciV9sPkC*3i0Gg_9G!=4ar*-W?D9)?EFL1=;O+W8}WGdp8TT!Fgv z{HKD`W>t(`Cds_qliEzuE!r{ihwEv1l5o~iqlgjAyGBi)$%zNvl~fSlg@M=C{TE;V zQkH`zS8b&!ut(m)%4n2E6MB>p*4(oV>+PT51#I{OXs9j1vo>9I<4CL1kv1aurV*AFZ^w_qfVL*G2rG@D2 zrs87oV3#mf8^E5hd_b$IXfH6vHe&lm@7On~Nkcq~YtE!}ad~?5*?X*>y`o;6Q9lkk zmf%TYonZM`{vJg$`lt@MXsg%*&zZZ0uUSse8o=!=bfr&DV)9Y6$c!2$NHyYAQf*Rs zk{^?gl9E z5Im8wlAsvQ6C2?DyG@95gUXZ3?pPijug25g;#(esF_~3uCj3~94}b*L>N2GSk%Qst z=w|Z>UX$m!ZOd(xV*2xvWjN&c5BVEdVZ0wvmk)I+YxnyK%l~caR=7uNQ=+cnNTLZ@&M!I$Mj-r{!P=; z`C2)D=VmvK8@T5S9JZoRtN!S*D_oqOxyy!q6Zk|~4aT|*iRN)fL)c>-yycR>-is0X zKrko-iZw(f(!}dEa?hef5yl%p0-v-8#8CX8!W#n2KNyT--^3hq6r&`)5Y@>}e^4h- zlPiDT^zt}Ynk&x@F8R&=)k8j$=N{w9qUcIc&)Qo9u4Y(Ae@9tA`3oglxjj6c{^pN( zQH+Uds2=9WKjH#KBIwrQI%bbs`mP=7V>rs$KG4|}>dxl_k!}3ZSKeEen4Iswt96GGw`E6^5Ov)VyyY}@itlj&sao|>Sb5 zeY+#1EK(}iaYI~EaHQkh7Uh>DnzcfIKv8ygx1Dv`8N8a6m+AcTa-f;17RiEed>?RT zk=dAksmFYPMV1vIS(Qc6tUO+`1jRZ}tcDP? zt)=7B?yK2RcAd1+Y!$K5*ds=SD;EEqCMG6+OqPoj{&8Y5IqP(&@zq@=A7+X|JBRi4 zMv!czlMPz)gt-St2VZwDD=w_S>gRpc-g zUd*J3>bXeZ?Psjohe;z7k|d<*T21PA1i)AOi8iMRwTBSCd0ses{)Q`9o&p9rsKeLaiY zluBw{1r_IFKR76YCAfl&_S1*(yFW8HM^T()&p#6y%{(j7Qu56^ZJx1LnN`-RTwimdnuo*M8N1ISl+$C-%=HLG-s} zc99>IXRG#FEWqSV9@GFW$V8!{>=lSO%v@X*pz*7()xb>=yz{E$3VE;e)_Ok@A*~El zV$sYm=}uNlUxV~6e<6LtYli1!^X!Ii$L~j4e{sI$tq_A(OkGquC$+>Rw3NFObV2Z)3Rt~Jr{oYGnZaFZ^g5TDZlg;gaeIP} z!7;T{(9h7mv{s@piF{-35L=Ea%kOp;^j|b5ZC#xvD^^n#vPH=)lopYz1n?Kt;vZmJ z!FP>Gs7=W{sva+aO9S}jh0vBs+|(B6Jf7t4F^jO3su;M13I{2rd8PJjQe1JyBUJ5v zcT%>D?8^Kp-70bP8*rulxlm)SySQhG$Pz*bo@mb5bvpLAEp${?r^2!Wl*6d7+0Hs_ zGPaC~w0E!bf1qFLDM@}zso7i~(``)H)zRgcExT_2#!YOPtBVN5Hf5~Ll3f~rWZ(UsJtM?O*cA1_W0)&qz%{bDoA}{$S&-r;0iIkIjbY~ zaAqH45I&ALpP=9Vof4OapFB`+_PLDd-0hMqCQq08>6G+C;9R~}Ug_nm?hhdkK$xpI zgXl24{4jq(!gPr2bGtq+hyd3%Fg%nofK`psHMs}EFh@}sdWCd!5NMs)eZg`ZlS#O0 zru6b8#NClS(25tXqnl{|Ax@RvzEG!+esNW-VRxba(f`}hGoqci$U(g30i}2w9`&z= zb8XjQLGN!REzGx)mg~RSBaU{KCPvQx8)|TNf|Oi8KWgv{7^tu}pZq|BS&S<53fC2K4Fw6>M^s$R$}LD*sUxdy6Pf5YKDbVet;P!bw5Al-8I1Nr(`SAubX5^D9hk6$agWpF}T#Bdf{b9-F#2WVO*5N zp+5uGgADy7m!hAcFz{-sS0kM7O)qq*rC!>W@St~^OW@R1wr{ajyYZq5H!T?P0e+)a zaQ%IL@X_`hzp~vRH0yUblo`#g`LMC%9}P;TGt+I7qNcBSe&tLGL4zqZqB!Bfl%SUa z6-J_XLrnm*WA`34&mF+&e1sPCP9=deazrM=Pc4Bn(nV;X%HG^4%Afv4CI~&l!Sjzb z{rHZ3od0!Al{}oBO>F*mOFAJrz>gX-vs!7>+_G%BB(ljWh$252j1h;9p~xVA=9_`P z5KoFiz96_QsTK%B&>MSXEYh`|U5PjX1(+4b#1PufXRJ*uZ*KWdth1<0 zsAmgjT%bowLyNDv7bTUGy|g~N34I-?lqxOUtFpTLSV6?o?<7-UFy*`-BEUsrdANh} zBWkDt2SAcGHRiqz)x!iVoB~&t?$yn6b#T=SP6Ou8lW=B>=>@ik93LaBL56ub`>Uo!>0@O8?e)$t(sgy$I z6tk3nS@yFFBC#aFf?!d_3;%>wHR;A3f2SP?Na8~$r5C1N(>-ME@HOpv4B|Ty7%jAv zR}GJwsiJZ5@H+D$^Cwj#0XA_(m^COZl8y7Vv(k=iav1=%QgBOVzeAiw zaDzzdrxzj%sE^c9_uM5D;$A_7)Ln}BvBx^=)fO+${ou%B*u$(IzVr-gH3=zL6La;G zu0Kzy5CLyNGoKRtK=G0-w|tnwI)puPDOakRzG(}R9fl7#<|oQEX;E#yCWVg95 z;NzWbyF&wGg_k+_4x4=z1GUcn6JrdX4nOVGaAQ8#^Ga>aFvajQN{!+9rgO-dHP zIp@%&ebVg}IqnRWwZRTNxLds+gz2@~VU(HI=?Epw>?yiEdZ>MjajqlO>2KDxA>)cj z2|k%dhh%d8SijIo1~20*5YT1eZTDkN2rc^zWr!2`5}f<2f%M_$to*3?Ok>e9$X>AV z2jYmfAd)s|(h?|B(XYrIfl=Wa_lBvk9R1KaP{90-z{xKi+&8=dI$W0+qzX|ZovWGOotP+vvYR(o=jo?k1=oG?%;pSqxcU* zWVGVMw?z__XQ9mnP!hziHC`ChGD{k#SqEn*ph6l46PZVkm>JF^Q{p&0=MKy_6apts z`}%_y+Tl_dSP(;Ja&sih$>qBH;bG;4;75)jUoVqw^}ee=ciV;0#t09AOhB^Py7`NC z-m+ybq1>_OO+V*Z>dhk}QFKA8V?9Mc4WSpzj{6IWfFpF7l^au#r7&^BK2Ac7vCkCn{m0uuN93Ee&rXfl1NBY4NnO9lFUp zY++C1I;_{#OH#TeP2Dp?l4KOF8ub?m6zE@XOB5Aiu$E~QNBM@;r+A5mF2W1-c7>ex zHiB=WJ&|`6wDq*+xv8UNLVUy4uW1OT>ey~Xgj@MMpS@wQbHAh>ysYvdl-1YH@&+Q! z075(Qd4C!V`9Q9jI4 zSt{HJRvZec>vaL_brKhQQwbpQd4_Lmmr0@1GdUeU-QcC{{8o=@nwwf>+dIKFVzPriGNX4VjHCa zTbL9w{Y2V87c2ofX%`(48A+4~mYTiFFl!e{3K^C_k%{&QTsgOd0*95KmWN)P}m zTRr{`f7@=v#+z_&fKYkQT!mJn{*crj%ZJz#(+c?>cD&2Lo~FFAWy&UG*Op^pV`BR^I|g?T>4l5;b|5OQ@t*?_Slp`*~Y3`&RfKD^1uLezIW(cE-Dq2z%I zBi8bWsz0857`6e!ahet}1>`9cYyIa{pe53Kl?8|Qg2RGrx@AlvG3HAL-^9c^1GW;)vQt8IK+ zM>!IW*~682A~MDlyCukldMd;8P|JCZ&oNL(;HZgJ>ie1PlaInK7C@Jg{3kMKYui?e!b`(&?t6PTb5UPrW-6DVU%^@^E`*y-Fd(p|`+JH&MzfEq;kikdse ziFOiDWH(D< zyV7Rxt^D0_N{v?O53N$a2gu%1pxbeK;&ua`ZkgSic~$+zvt~|1Yb=UfKJW2F7wC^evlPf(*El+#}ZBy0d4kbVJsK- z05>;>?HZO(YBF&v5tNv_WcI@O@LKFl*VO?L(!BAd!KbkVzo;v@~3v`-816GG?P zY+H3ujC>5=Am3RIZDdT#0G5A6xe`vGCNq88ZC1aVXafJkUlcYmHE^+Z{*S->ol%-O znm9R0TYTr2w*N8Vs#s-5=^w*{Y}qp5GG)Yt1oLNsH7y~N@>Eghms|K*Sdt_u!&I}$ z+GSdFTpbz%KH+?B%Ncy;C`uW6oWI46(tk>r|5|-K6)?O0d_neghUUOa9BXHP*>vi; z={&jIGMn-92HvInCMJcyXwHTJ42FZp&Wxu+9Rx;1x(EcIQwPUQ@YEQQ`bbMy4q3hP zNFoq~Qd0=|xS-R}k1Im3;8s{BnS!iaHIMLx)aITl)+)?Yt#fov|Eh>}dv@o6R{tG>uHsy&jGmWN5+*wAik|78(b?jtysPHC#e+Bzz~V zS3eEXv7!Qn4uWi!FS3B?afdD*{fr9>B~&tc671fi--V}~E4un;Q|PzZRwk-azprM$4AesvUb5`S`(5x#5VJ~4%ET6&%GR$}muHV-5lTsCi_R|6KM(g2PCD@|yOpKluT zakH!1V7nKN)?6JmC-zJoA#ciFux8!)ajiY%K#RtEg$gm1#oKUKX_Ms^%hvKWi|B=~ zLbl-L)-=`bfhl`>m!^sRR{}cP`Oim-{7}oz4p@>Y(FF5FUEOfMwO!ft6YytF`iZRq zfFr{!&0Efqa{1k|bZ4KLox;&V@ZW$997;+Ld8Yle91he{BfjRhjFTFv&^YuBr^&Pe zswA|Bn$vtifycN8Lxr`D7!Kygd7CuQyWqf}Q_PM}cX~S1$-6xUD%-jrSi24sBTFNz(Fy{QL2AmNbaVggWOhP;UY4D>S zqKr!UggZ9Pl9Nh_H;qI`-WoH{ceXj?m8y==MGY`AOJ7l0Uu z)>M%?dtaz2rjn1SW3k+p`1vs&lwb%msw8R!5nLS;upDSxViY98IIbxnh{}mRfEp=9 zbrPl>HEJeN7J=KnB6?dwEA6YMs~chHNG?pJsEj#&iUubdf3JJwu=C(t?JpE6xMyhA3e}SRhunDC zn-~83*9=mADUsk^sCc%&&G1q5T^HR9$P#2DejaG`Ui*z1hI#h7dwpIXg)C{8s< z%^#@uQRAg-$z&fmnYc$Duw63_Zopx|n{Bv*9Xau{a)2%?H<6D>kYY7_)e>OFT<6TT z0A}MQLgXbC2uf`;67`mhlcUhtXd)Kbc$PMm=|V}h;*_%vCw4L6r>3Vi)lE5`8hkSg zNGmW-BAOO)(W((6*e_tW&I>Nt9B$xynx|sj^ux~?q?J@F$L4;rnm_xy8E*JYwO-02u9_@@W0_2@?B@1J{y~Q39N3NX^t7#`=34Wh)X~sU&uZWgS1Z09%_k|EjA4w_QqPdY`oIdv$dJZ;(!k)#U8L+|y~gCzn+6WmFt#d{OUuKHqh1-uX_p*Af8pFYkYvKPKBxyid4KHc}H` z*KcyY;=@wzXYR{`d{6RYPhapShXIV?0cg_?ahZ7do)Ot#mxgXYJYx}<%E1pX;zqHd zf!c(onm{~#!O$2`VIXezECAHVd|`vyP)Uyt^-075X@NZDBaQt<>trA3nY-Dayki4S zZ^j6CCmx1r46`4G9794j-WC0&R9(G7kskS>=y${j-2;(BuIZTLDmAyWTG~`0)Bxqk zd{NkDe9ug|ms@0A>JVmB-IDuse9h?z9nw!U6tr7t-Lri5H`?TjpV~8(gZWFq4Vru4 z!86bDB;3lpV%{rZ`3gtmcRH1hjj!loI9jN>6stN6A*ujt!~s!2Q+U1(EFQEQb(h4E z6VKuRouEH`G6+8Qv2C)K@^;ldIuMVXdDDu}-!7FS8~k^&+}e9EXgx~)4V4~o6P^52 z)a|`J-fOirL^oK}tqD@pqBZi_;7N43%{IQ{v&G9^Y^1?SesL`;Z(dt!nn9Oj5Odde%opv&t zxJ><~b#m+^KV&b?R#)fRi;eyqAJ_0(nL*61yPkJGt;gZxSHY#t>ATnEl-E%q$E16% zZdQfvhm5B((y4E3Hk6cBdwGdDy?i5CqBlCVHZr-rI$B#>Tbi4}Gcvyg_~2=6O9D-8 zY2|tKrNzbVR$h57R?Pe+gUU_il}ZaWu|Az#QO@};=|(L-RVf0AIW zq#pO+RfM7tdV`9lI6g;{qABNId`fG%U9Va^ravVT^)CklDcx)YJKeJdGpM{W1v8jg z@&N+mR?BPB=K1}kNwXk_pj44sd>&^;d!Z~P>O78emE@Qp@&8PyB^^4^2f7e)gekMv z2aZNvP@;%i{+_~>jK7*2wQc6nseT^n6St9KG#1~Y@$~zR_=AcO2hF5lCoH|M&c{vR zSp(GRVVl=T*m~dIA;HvYm8HOdCkW&&4M~UDd^H)`p__!4k+6b)yG0Zcek8OLw$C^K z3-BbLiG_%qX|ZYpXJ$(c@aa7b4-*IQkDF}=gZSV`*ljP|5mWuHSCcf$5qqhZTv&P?I$z^>}qP(q!Aku2yA5vu38d8x*q{6-1`%PrE_r0-9Qo?a#7Zbz#iGI7K<(@k^|i4QJ1H z4jx?{rZbgV!me2VT72@nBjucoT zUM9;Y%TCoDop?Q5fEQ35bCYk7!;gH*;t9t-QHLXGmUF;|vm365#X)6b2Njsyf1h9JW#x$;@x5Nx2$K$Z-O3txa%;OEbOn6xBzd4n4v)Va=sj5 z%rb#j7{_??Tjb8(Hac<^&s^V{yO-BL*uSUk2;X4xt%NC8SjO-3?;Lzld{gM5A=9AV z)DBu-Z8rRvXXwSVDH|dL-3FODWhfe1C_iF``F05e{dl(MmS|W%k-j)!7(ARkV?6r~ zF=o42y+VapxdZn;GnzZfGu<6oG-gQ7j7Zvgo7Am@jYxC2FpS@I;Jb%EyaJDBQC(q% zKlZ}TVu!>;i3t~OAgl@QYy1X|T~D{HOyaS*Bh}A}S#a9MYS{XV{R-|niEB*W%GPW! zP^NU(L<}>Uab<;)#H)rYbnqt|dOK(-DCnY==%d~y(1*{D{Eo1cqIV8*iMfx&J*%yh zx=+WHjt0q2m*pLx8=--UqfM6ZWjkev>W-*}_*$Y(bikH`#-Gn#!6_ zIA&kxn;XYI;eN9yvqztK-a113A%97in5CL5Z&#VsQ4=fyf&3MeKu70)(x^z_uw*RG zo2Pv&+81u*DjMO6>Mrr7vKE2CONqR6C0(*;@4FBM;jPIiuTuhQ-0&C)JIzo_k>TaS zN_hB;_G=JJJvGGpB?uGgSeKaix~AkNtYky4P7GDTW6{rW{}V9K)Cn^vBYKe*OmP!; zohJs=l-0sv5&pL6-bowk~(swtdRBZQHh8)m^r2+qTtZ zt4m$B?OQYNyfBA0E)g28a*{)a=%%f-?{F;++-Xs#5|7kSHTD*E9@$V ztE%7zX4A(L`n)FY8Y4pOnKC|Pf)j$iR#yP;V0+|Hki+D;t4I4BjkfdYliK9Gf6RYw z;3px$Ud5aTd`yq$N7*WOs!{X91hZZ;AJ9iQOH%p;v$R%OQum_h#rq9*{ve(++|24z zh2P;{-Z?u#rOqd0)D^_Ponv(Y9KMB9#?}nJdUX&r_rxF0%3__#8~ZwsyrSPmtWY27 z-54ZquV2t_W!*+%uwC=h-&_q~&nQer0(FL74to%&t^byl^C?wTaZ-IS9OssaQFP)1 zAov0o{?IRAcCf+PjMWSdmP42gysh|c9Ma&Q^?_+>>+-yrC8WR;*XmJ;>r9v*>=W}tgWG;WIt{~L8`gk8DP{dSdG z4SDM7g5ahMHYHHk*|mh9{AKh-qW7X+GEQybJt9A@RV{gaHUAva+=lSroK^NUJYEiL z?X6l9ABpd)9zzA^;FdZ$QQs#uD@hdcaN^;Q=AXlbHv511Meye`p>P4Y2nblEDEeZo}-$@g&L98Aih6tgLz--${eKTxymIipy0xSYgZZ zq^yyS4yNPTtPj-sM?R8@9Q1gtXPqv{$lb5i|C1yymwnGdfYV3nA-;5!Wl zD0fayn!B^grdE?q^}ba{-LIv*Z}+hZm_F9c$$cW!bx2DgJD&6|bBIcL@=}kQA1^Eh zXTEznqk)!!IcTl>ey?V;X8k<+C^DRA{F?T*j0wV`fflrLBQq!l7cbkAUE*6}WabyF zgpb+|tv=aWg0i}9kBL8ZCObYqHEycr5tpc-$|vdvaBsu#lXD@u_e1iL z{h>xMRS0a7KvW?VttrJFpX^5DC4Bv4cp6gNG6#8)7r7IxXfSNSp6)_6tZ4l>(D+0I zPhU)N!sKywaBusHdVE!yo5$20JAU8V_XcW{QmO!p*~ns8{2~bhjydnmA&=r zX9NSM9QYogYMDZ~kS#Qx`mt>AmeR3p@K$`fbJ%LQ1c5lEOz<%BS<}2DL+$>MFcE%e zlxC)heZ7#i80u?32eOJI9oQRz0z;JW@7Th4q}YmQ-`Z?@y3ia^_)7f37QMwDw~<-@ zT)B6fftmK_6YS!?{uaj5lLxyR++u*ZY2Mphm5cd7PA5=%rd)95hJ9+aGSNfjy>Ylc zoI0nGIT3sKmwX8h=6CbvhVO+ehFIR155h8iRuXZx^cW>rq5K4z_dvM#hRER=WR@THs%WELI9uYK9HN44Em2$#@k)hD zicqRPKV#yB;UlcsTL_}zCMK0T;eXHfu`y2(dfwm(v)IBbh|#R>`2cot{m7}8_X&oD zr@94PkMCl%d3FsC4pil=#{3uv^+)pvxfwmPUr)T)T|GcZVD$wVj$mjkjDs`5cm8N! zXVq2CvL;gWGpPI4;9j;2&hS*o+LNp&C5Ac=OXx*W5y6Z^az)^?G0)!_iAfjH5wiSE zD(F}hQZB#tF5iEx@0sS+dP70DbZ*<=5X^)Pxo^8aKzOzuyc2rq=<0-k;Y_ID1>9^v z+)nc36}?>jen*1%OX3R*KRASj${u$gZ$27Hpcj=95kK^aLzxhW6jj_$w6}%#1*$5D zG1H_vYFrCSwrRqYw*9<}OYAOQT)u%9lC`$IjZV<4`9Sc;j{Qv_6+uHrYifK&On4V_7yMil!0Yv55z@dFyD{U@Sy>|vTX=P_( zRm<2xj*Z}B30VAu@0e+}at*y?wXTz|rPalwo?4ZZc>hS0Ky6~mi@kv#?xP2a;yt?5=(-CqvP_3&$KdjB7Ku;# z`GLE*jW1QJB5d&E?IJO?1+!Q8HQMGvv^RuFoi=mM4+^tOqvX%X&viB%Ko2o-v4~~J z267ui;gsW?J=qS=D*@*xJvAy3IOop5bEvfR4MZC>9Y4Z$rGI|EHNNZ7KX;Ix{xSvm z-)Cau-xuTm|7`4kUdXvd_d^E=po(76ELfq5OgxIt3aqDy#zBfIy-5<3gpn{Ce`-ha z<;6y@{Bgqw?c~h*&j{FozQCh=`Lv-5Iw!KdSt;%GDOq%=(V!dJ-}|}|0o5G2kJj6{ z`jCSPs$9Fe8O(+qALZiJ$WtR=<@GvsdM)IJ`7XrBfW0iyYE#Vy^e@zbysg*B5Z_kSL6<)vqoaH zQ{!9!*{e9UZo^h+qZ`T@LfVwAEwc&+9{C8c%oj41q#hyn<&zA9IIur~V|{mmu`n5W z8)-Ou$YgjQ*PMIqHhZ_9E?(uoK0XM5aQkarcp}WT^7b^FC#^i>#8LGZ9puDuXUYas z7caX)V5U6uY-L5Wl%)j$qRkR;7@3T*N64YK_!`Fw=>CAwe~2loI1<>DZW&sb7Q)X;6E08&$h! z2=c1i4UOO{R4TmkTz+o9n`}+%d%blR6P;5{`qjtxlN$~I%tMMDCY`~e{+mRF!rj5( z3ywv)P_PUUqREu)TioPkg&5RKjY6z%pRxQPQ{#GNMTPag^S8(8l{!{WGNs2U1JA-O zq02VeYcArhTAS;v3);k(&6ayCH8SXN@r;1NQeJ*y^NHM+zOd;?t&c!Hq^SR_w6twGV8dl>j zjS+Zc&Yp7cYj&c1y3IxQ%*kWiYypvoh(k8g`HrY<_Bi-r%m-@SLfy-6mobxkWHxyS z>TtM2M4;Uqqy|+8Q++VcEq$PwomV1D4UzNA*Tgkg9#Gpz#~&iPf|Czx!J?qss?e|3 z4gTua75-P{2X7w9eeK3~GE0ip-D;%%gTi)8bR~Ez@)$gpuS~jZs`CrO5SR-Xy7bkA z89fr~mY}u4A$|r1$fe-;T{yJh#9Ime1iRu8eo?uY9@yqAU3P!rx~SsP;LTBL zeoMK(!;(Zt8313 z3)V)q_%eflKW?BnMZa}6E0c7t!$-mC$qt44OME5F(6B$E8w*TUN-h}0dOiXI+TH zYFrr&k1(yO(|J0vP|{22@Z}bxm@7BkjO)f)&^fv|?_JX+s)1*|7X7HH(W?b3QZ3!V|~m?8}uJsF>NvE4@fik zjyyh+U*tt`g6v>k9ub88a;ySvS1QawGn7}aaR**$rJA=a#eUT~ngUbJ%V=qsFIekLbv!YkqjTG{_$F;$w19$(ivIs*1>?2ka%uMOx@B9`LD zhm~)z@u4x*zcM1WhiX)!U{qOjJHt1xs{G1S?rYe)L)ntUu^-(o_dfqZu)}W(X%Uu| zN*qI@&R2fB#Jh|Mi+eMrZDtbNvYD3|v0Kx>E#Ss;Be*T$@DC!2A|mb%d}TTN3J+c= zu@1gTOXFYy972S+=C;#~)Z{Swr0VI5&}WYzH22un_Yg5o%f9fvV(`6!{C<(ZigQ2`wso)cj z9O12k)15^Wuv#rHpe*k5#4vb%c znP+Gjr<-p%01d<+^yrSoG?}F=eI8X;?=Fo2a~HUiJ>L!oE#9tXRp!adg-b9D;(6$E zeW0tH$US04zTX$OxM&X+2ip>KdFM?iG_fgOD-qB|uFng8*#Z5jgqGY=zLU?4!OlO#~YBTB9b9#~H@nqQ#5 z6bV));d?IJTVBC+79>rGuy1JgxPLy$dA7;_^^L)02m}XLjFR*qH`eI~+eJo(7D`LH z(W%lGnGK+Vk_3kyF*zpgO=1MxMg?hxe3}}YI>dVs8l}5eWjYu4=w6MWK09+05 zGdpa#$awd>Q|@aZa*z{5F3xy3n@E4YT9%TmMo0jxW59p0bI?&S}M+ z&^NG%rf7h*m9~p#b19|`wO5OMY-=^XT+=yrfGNpl<&~~FGsx_`IaFn+sEgF$hgOa~oAVAiu^a$jHcqkE=dj`ze z=axsfrzzh6VGD0x#6Ff=t%+VTiq!n6^gv*uIUD<9fOhvR;al5kcY${uunn}-!74<7 zmP^3cl-kyN(QY!!Z-^PY-OUkh=3ZWk6>le$_Q&xk4cgH{?i)C%2RM@pX5Q{jdSlo! zVau5v44cQX5|zQlQDt;dCg)oM0B<=P1CR!W%!^m$!{pKx;bn9DePJjWBX)q!`$;0K zqJIIyD#aK;#-3&Nf=&IhtbV|?ZGYHSphp~6th`p2rkw&((%kBV7<{siEOU7AxJj+FuRdDu$ zcmTW8usU_u!r)#jg|J=Gt{##7;uf4A5cdt6Y02}f(d2)z~ z)CH~gVAOwBLk$ZiIOn}NzDjvfw(w$u|BdCBI#)3xB-Ot?nz?iR38ayCm48M=_#9r7 zw8%pwQ<9mbEs5~_>pN3~#+Er~Q86J+2TDXM6umCbukd-X6pRIr5tF?VauT8jW> zY^#)log>jtJs2s3xoiPB7~8#1ZMv>Zx0}H58k-@H2huNyw~wsl0B8j)H5)H9c7y&i zp8^0;rKbxC1eEZ-#Qxvz)Xv$((8lK9I>BspPajluysw^f#t9P;OUis43mmEzX+lk* zc4T-Ms9_687GR+~QS#0~vxK#DSGN=a-m(@eZTqw2<+lN9>R~gK2)3;sT4%nI%Y|0m zX9SPR!>?~s=j5H4WMqeTW8QaLZ=1bWS5I3xZ&$(ypc=tHrv+hX@s)VG(tc!yvLM7n zshN=C#v={X1r;)xn0Pow_1eMhkn!{;x$BJ#PIz)m585&%cmzk;btQzZAN_^zis;n? z?6I~bN?s;7vg_dtoTc4A5Ow*Rb}No#UYl)sN|RmoYo}k^cKLXd8F`44?RrokkPvN5 ztUrx;U~B;jbE_qGd3n0j2i}A{enJvJ?gSF~NQj~EP5vM-w4@;QQ5n(Npic}XNW6B0 zq9F4T%6kp7qGhd0vpQcz+nMk8GOAmbz8Bt4@GtewGr6_>Xj>ge)SyfY}nu>Y!a@HoIx(StD zx`!>RT&}tpBL%nOF%7XIFW?n1AP*xthCMzhrU6G!U6?m4!CPWTvn#Yaoi_95CT2!L z|B=5zeRW30&ANGN>J9#GtCm&3SF6n4TqDz<-{@ZXkrkRDCpV$DwCtI^e&3i1A{Ar&JZtS^c+lyPa6 z%JJr42S_;eFC#M~bdtQePhOU32WDiZ4@H&af)z#$Y|hnQNb)8(3?1Ad>5uaZ1z zU~!jt3XUI@gpWb8tWTyH7DGvKvzYfqNIy3P{9vpwz_C-QL&`+8Io$F5PS-@YQJoEO z17D9P(+sXajWSH_8&C?fn>rTLX+(?KiwX#JNV)xE0!Q@>Tid$V2#r4y6fkph?YZ>^ z(o^q(0*P->3?I0cELXJn(N|#qTm6 zAPIL~n)m!50;*?5=MOOc4Wk;w(0c$(!e?vpV23S|n|Y7?nyc8)fD8t-KI&nTklH&BzqQ}D(1gH3P+5zGUzIjT~x`;e8JH=86&5&l-DP% z)F+Et(h|GJ?rMy-Zrf>Rv@<3^OrCJ1xv_N*_@-K5=)-jP(}h1Rts44H&ou8!G_C1E zhTfUDASJ2vu!4@j58{NN;78i?6__xR75QEDC4JN{>RmgcNrn-EOpEOcyR<8FS@RB@ zH!R7J=`KK^u06eeI|X@}KvQmdKE3AmAy8 zM4IIvde#e4O(iwag`UL5yQo>6&7^=D4yE-Eo9$9R2hR} zn;Z9i-d=R-xZl4@?s%8|m1M`$J6lW1r0Y)+8q$}Vn4qyR1jqTjGH;@Z!2KiGun2~x zaiEfzVT<|_b6t}~XPeflAm8hvCHP3Bp*tl{^y_e{Jsn@w+KP{7}bH_s=1S2E1sj=18a39*Ag~lbkT^_OQuYQey=b zW^{0xlQ@O$^cSxUZ8l(Mspg8z0cL*?yH4;X2}TdN)uN31A%$3$a=4;{S@h#Y(~i%) zc=K7Ggl=&2hYVic*W65gpSPE70pU;FN@3k?BYdNDKv6wlsBAF^);qiqI zhklsX4TaWiC%VbnZ|yqL+Pcc;(#&E*{+Rx&<&R{uTYCn^OD|mAk4%Q7gbbgMnZwE{ zy7QMK%jIjU@ye?0; z;0--&xVeD}m_hq9A8a}c9WkI2YKj8t!Mkk!o%AQ?|CCBL9}n570}OmZ(w)YI6#QS&p<={tcek*D{CPR%eVA1WBGUXf z%gO2vL7iVDr1$!LAW)1@H>GoIl=&yyZ7=*9;wrOYQ}O}u>h}4FWL?N2ivURlUi11- zl{G0fo`9?$iAEN<4kxa#9e0SZPqa{pw?K=tdN5tRc7HDX-~Ta6_+#s9W&d`6PB7dF*G@|!Mc}i zc=9&T+edI(@la}QU2An#wlkJ&7RmTEMhyC_A8hWM54?s1WldCFuBmT5*I3K9=1aj= z6V@93P-lUou`xmB!ATp0(We$?)p*oQs;(Kku15~q9`-LSl{(Efm&@%(zj?aK2;5}P z{6<@-3^k^5FCDT@Z%XABEcuPoumYkiD&)-8z2Q}HO9OVEU3WM;V^$5r4q>h^m73XF z5!hZ7SCjfxDcXyj(({vg8FU(m2_}36L_yR>fnW)u=`1t@mPa76`2@%8v@2@$N@TE` z)kYhGY1jD;B9V=Dv1>BZhR9IJmB?X9Wj99f@MvJ2Fim*R`rsRilvz_3n!nPFLmj({EP!@CGkY5R*Y_dSO{qto~WerlG}DMw9k+n}pk z*nL~7R2gB{_9=zpqX|*vkU-dx)(j+83uvYGP?K{hr*j2pQsfXn<_As6z%-z+wFLqI zMhTkG>2M}#BLIOZ(ya1y8#W<+uUo@(43=^4@?CX{-hAuaJki(_A(uXD(>`lzuM~M;3XA48ZEN@HRV{1nvt?CV)t;|*dow0Ue2`B*iA&!rI`fZQ=b28= z_dxF}iUQ8}nq0SA4NK@^EQ%=)OY;3fC<$goJ&Kp|APQ@qVbS-MtJQBc)^aO8mYFsbhafeRKdHPW&s^&;%>v zlTz`YE}CuQ@_X&mqm{+{!h2r)fPGeM_Ge4RRYQkrma`&G<>RW<>S(?#LJ}O-t)d$< zf}b0svP^Zu@)MqwEV^Fb_j zPYYs~vmEC~cOIE6Nc^@b@nyL!w5o?nQ!$mGq(Pa|1-MD}K0si<&}eag=}WLSDO zE4+eA~!J(K}605x&4 zT72P7J^)Y)b(3g2MZ@1bv%o1ggwU4Yb!DhQ=uu-;vX+Ix8>#y6wgNKuobvrPNx?$3 zI{BbX<=Y-cBtvY&#MpGTgOLYU4W+csqWZx!=AVMb)Z;8%#1*x_(-)teF>45TCRwi1 z)Nn>hy3_lo44n-4A@=L2gI$yXCK0lPmMuldhLxR8aI;VrHIS{Dk}yp= zwjhB6v@0DN=Hnm~3t>`CtnPzvA*Kumfn5OLg&-m&fObRD};c}Hf?n&mS< z%$wztc%kjWjCf-?+q(bZh9k~(gs?i4`XVfqMXvPVkUWfm4+EBF(nOkg!}4u)6I)JT zU6IXqQk?p1a2(bz^S;6ZH3Wy9!JvbiSr7%c$#G1eK2^=~z1WX+VW)CPD#G~)13~pX zErO(>x$J_4qu-)lNlZkLj2}y$OiKn0ad5Imu5p-2dnt)(YI|b7rJ3TBUQ8FB8=&ym50*ibd2NAbj z;JA&hJ$AJlldM+tO;Yl3rBOFiP8fDdF?t(`gkRpmT9inR@uX{bThYNmxx-LN5K8h0 ztS%w*;V%b`%;-NARbNXn9he&AO4$rvmkB#;aaOx?Wk|yBCmN{oMTK&E)`s&APR<-5 z#;_e75z;LJ)gBG~h<^`SGmw<$Z3p`KG|I@7Pd)sTJnouZ1hRvm3}V+#lPGk4b&A#Y z4VSNi8(R1z7-t=L^%;*;iMTIAjrXl;h106hFrR{n9o8vlz?+*a1P{rEZ2ie{luQs} zr6t746>eoqiO5)^y;4H%2~&FT*Qc*9_oC2$+&syHWsA=rn3B~4#QEW zf4GT3i_@)f(Fj}gAZj`7205M8!B&HhmbgyZB& z+COyAVNxql#DwfP;H48Yc+Y~ChV6b9auLnfXXvpjr<~lQ@>VbCpQvWz=lyVf1??_c zAo3C^otZD@(v?X)UX*@w?TF|F8KF>l7%!Dzu+hksSA^akEkx8QD(V(lK+HBCw6C}2onVExW)f$ zncm*HI(_H;jF@)6eu}Tln!t?ynRkcqBA5MitIM@L^(4_Ke}vy7c%$w{(`&7Rn=u>oDM+Z^RUYcbSOPwT(ONyq76R>$V6_M_UP4vs=__I#io{{((| zy5=k=oVr-Qt$FImP~+&sN8rf2UH*vRMpwohPc@9?id17La4weIfBNa>1Djy+1=ugn z@}Zs;eFY1OC}WBDxDF=i=On_33(jWE-QYV)HbQ^VM!n>Ci9_W0Zofz7!m>do@KH;S z4k}FqEAU2)b%B_B-QcPnM5Zh=dQ+4|DJoJwo?)f2nWBuZE@^>a(gP~ObzMuyNJTgJFUPcH`%9UFA(P23iaKgo0)CI!SZ>35LpFaD7 z)C2sW$ltSEYNW%%j8F;yK{iHI2Q^}coF@LX`=EvxZb*_O;2Z0Z5 z7 zlccxmCfCI;_^awp|G748%Wx%?t9Sh8!V9Y(9$B?9R`G)Nd&snX1j+VpuQ@GGk=y(W zK|<$O`Cad`Y4#W3GKXgs%lZduAd1t1<7LwG4*zaStE*S)XXPFDyKdgiaVXG2)LvDn zf}eQ_S(&2!H0Mq1Yt&WpM1!7b#yt_ie7naOfX129_E=)beKj|p1VW9q>>+e$3@G$K zrB%i_TT1DHjOf7IQ8)Wu4#K%ZSCDGMP7Ab|Kvjq7*~@ewPm~h_-8d4jmNH<&mNZC@CI zKxG5O08|@<4(6IEC@L-lcrrvix&_Dj4tBvl=8A}2UX|)~v#V$L22U}UHk`B-1MF(t zU6aVJWR!>Y0@4m0UA%Sq9B5;4hZvsOu=>L`IU4#3r_t}os|vSDVMA??h>QJ1FD1vR z*@rclvfD!Iqoxh>VP+?b9TVH8g@KjYR@rRWQy44A`f6doIi+8VTP~pa%`(Oa@5?=h z8>YxNvA##a3D0)^P|2|+0~f|UsAJV=q(S>eq-dehQ+T>*Q@qN zU8@kdpU5gGk%ozt?%c8oM6neA?GuSsOfU_b1U)uiEP8eRn~>M$p*R z43nSZs@^ahO78s zulbK@@{3=2=@^yZ)DuIC$ki;`2WNbD_#`LOHN9iMsrgzt-T<8aeh z(oXrqI$Kgt6)Icu=?11NWs>{)_ed1wh>)wv6RYNUA-C&bejw{cBE_5Wzeo!AHdTd+ z)d(_IKN7z^n|As~3XS=cCB_TgM7rK;X586re`{~Foml$aKs zb!4Pe7hEP|370EWwn$HKPM!kL94UPZ1%8B^e5fB+=Iw^6=?5n3tZGYjov83CLB&OQ++p)WCMeshCv_9-~G9C_2x`LxTDjUcW$l6e!6-&a^fM3oP9*g(H zmCk0nGt1UMdU#pfg1G0um5|sc|KO<+qU1E4iBF~RvN*+`7uNHH^gu{?nw2DSCjig% zI@ymKZSK=PhHJa(jW&xeApv&JcfSmNJ4uQ|pY=Lcc>=J|{>5Ug3@x#R_b@55xFgfs za^ANzWdD$ZYtFs$d7+oiw0ZmPk2&l|< zc8()wfiJx@EGpQT zG$8iLkQZ-086doF1R zh<#9cz_vRsJdoXbD=QgOtpm}cFAJX8c}>Jew;PQJSXSb^;wlC zxXLHTS|!GZ-VK_4wV<9bk4RUmlsByzW_^b>)$6R+jQ}^wco1nMA`9Lncs;&QGp!`5Tx#aXXU?}5_RrtUY zx(EMzDhl-a^y^f5yfFLMnOO#u)l69&4M?|ne|2EV>zQ}4JQCBel?~2I4?D|>L$%H(peOOII!U}i z-j)*h1rODe9{0`xmhG;`AKqw1p0_KhEIU8)DoGnEn9wAhXPaxO_(jNSij~J5m$P*$ z9Mt(t;eV}2+i|kjQpBFcNb7_(VbuF<;RQB~R~p>2*Lg>a&7DEEuq*I%Ls4{zHeUDq z+M0&YhEn^C*9-B4Q7HJ$xj)dORCXPK+)ZtLOa0o&)Sl+f(Y{p*68$-#yagW5^HQnQ z0pWpoQpxg8<&gx9im(>=x6v#&RbQ7^AsjxeSDA? zi4MEJUC~ByG!PiBjq7$pK&FA^5 z=Y@dtQnuy%IfsaR`TVP0q^3mixl&J-3!$H!ua#{A>0Z1JdLq#d4UV9nlYm641ZHl zH6mK~iI6lR3OUEVL}Z5{ONZ_6{Nk%Bv03ag<1HVN?R%w2^aR5@E>6(r>}IoMl$wRF zWr-DItN*k7T$NTT8B)+23c?171sADhjInb2Xb>GhFYGC&3{b>huvLlaS4O z^{j5q+b5H?Z)yuy%AByaVl2yj9cnalY1sMQ zXI#e%*CLajxGxP!K6xf9RD2pMHOfAa1d^Lr6kE`IBpxOiGXfNcoQ*FI6wsNtLD!T+ zC4r2q>5qz0f}UY^RY#1^0*FPO*Zp-U1h9U|qWjwqJaDB(pZ`<`U-xo7+JB$zvwV}^ z2>$0&Q5k#l|Er7*PPG1ycj4BGz zg&`d*?nUi1Q!OB>{V@T$A;)8@h;*Rb1{xk_8X<34L`s}xkH-rQZvjM`jI=jaJRGRg zeEcjYChf-78|RLrao%4HyZBfnAx5KaE~@Sx+o-2MLJ>j-6uDb!U`odj*=)0k)K75l zo^)8-iz{_k7-_qy{Ko~N#B`n@o#A22YbKiA>0f3k=p-B~XX=`Ug>jl$e7>I=hph0&AK z?ya;(NaKY_!od=tFUcGU5Kwt!c9EPUQLi;JDCT*{90O@Wc>b| zI;&GIY$JlQW^9?R$-OEUG|3sp+hn+TL(YK?S@ZW<4PQa}=IcUAn_wW3d!r#$B}n08 z*&lf(YN21NDJ74DqwV`l`RX(4zJ<(E4D}N0@QaE-hnfdPDku~@yhb^AeZL73RgovX z6=e>!`&e^l@1WA5h!}}PwwL*Gjg!LbC5g0|qb8H$^S{eGs%cc?4vTyVFW=s6KtfW? z@&Xm+E(uz(qDbwDvRQI9DdB<2sW}FYK9sg*f%-i*>*n{t-_wXvg~N7gM|a91B!x|K zyLbJ~6!!JZpZ`#HpCB8g#Q*~VU47Rp$NyZb3WhEgg3ivSwnjGJgi0BEV?!H}Z@QF| zrO`Kx*52;FR#J-V-;`oR-pr!t>bYf)UYcixN=(FUR6$fhN@~i09^3WeP3*)D*`*mJ z1u%klAbzQ=P4s%|FnVTZv%|@(HDB+ap5S#cFSJUSGkyI*Y>9Lwx|0lTs%uhoCW(f1 zi+|a9;vDPfh3nS<7m~wqTM6+pEm(&z-Ll;lFH!w#(Uk#2>Iv~2Hu}lITn7hnOny`~ z*Vj=r<&Nwpq^@g5m`u&QTBRoK*}plAuHg$L$~NO#wF0!*r0OfcS%)k0A??uY*@B^C zJe9WdU(w){rTIf<;rwJt^_35^d<A@$FqEZW6kwyfAo2x0T$Ye2MZox6Z7<%Qbu$}}u{rtE+h2M+Z}T4I zxF1cwJ(Uvp!T#mogWkhb(?SxD4_#tV(Sc8N4Gu*{Fh#})Pvb^ef%jrlnG*&Ie+J5 zsly5oo?1((um&lLDxn(DkYtk`My>lgKTp3Y4?hTQ4_`YNOFtjF-FUY#d#(EQd(rfz zB8z%Vi;?x)ZM$3c>yc5H8KBvSevnWNdCbAj?QCac)6-K~Xz@EZp}~N9q)5*Ufjz3C z6kkOeI{3H(^VO8hKDrVjy2DXd;5wr4nb`19yJi0DO@607MSx+7F$ zz3F7sl8JV@@sM$6`#JmSilqI%Bs)}Py2eFT;TjcG5?8$zwV60b(_5A>b#uk~7U^bO z>y|6SCrP2IGST(8HFuX|XQUXPLt2gL_hm|uj1Ws`O2VW>SyL^uXkl>Zvkcpi?@!F7 z%svLoT@{R#XrIh^*dE~$YhMwC+b7JE09NAS47kT%Ew zD!XjxA@1+KOAyu`H2z#h+pGm!lG>WI0v745l+Fd><3dh{ATq%h?JSdEt zu%J*zfFUx%Tx&0DS5WSbE)vwZSoAGT=;W#(DoiL($BcK;U*w`xA&kheyMLI673HCb7fGkp{_vdV2uo;vSoAH z9BuLM#Vzwt#rJH>58=KXa#O;*)_N{$>l7`umacQ0g$pI3iW4=L--O;Wiq0zy7OKp`j2r^y3`7X!?sq9rr5B{41BkBr1fEd1#Q3 z-dXc2RSb4U>FvpVhlQCIzQ-hs=8420z=7F2F(^xD;^RXgpjlh8S6*xCP#Gj2+Q0bAg?XARw3dnlQ*Lz3vk}m`HXmCgN=?bIL{T zi}Ds-xn|P)dxhraT@XY$ZQ&^%x8y!o+?n#+>+dZ1c{hYwNTNRke@3enT(a@}V*X{! z81+{Jc2UR;+Zcbc6cUlafh4DFKwp>;M}8SGD+YnW3Q_)*9Z_pny_z+MeYQmz?r%EVaN0d!NE*FVPq&U@vo{ef6wkMIDEWLbDs zz91$($XbGnQ?4WHjB~4xgPgKZts{p|g1B{-4##}#c5aL5C6_RJ_(*5>85B1}U!_<``}q-97Q7~u)(&lsb(WT^(*n7H%33%@_b zO5(?-v??s??33b19xiB7t_YT!q8!qAzN1#RD@3;kYAli%kazt#YN7}MhVu=ljuz27 z1`<+g8oVwy57&$`CiHeaM)tz(OSt4E# zJ@P6E*e504oUw~RD(=9WP8QdW^6wRdFbKII!GAWecJ(?{`EzTR@?j!3g?$@LLCt;U={>!9z7DU!(1Jq zqEwdx5q?W1Ncm7mXP8MFwAr?nw5$H%cb>Q><9j{Tk2RY9ngGvaJgWXx^r!ywk{ph- zs2PFto4@IIwBh{oXe;yMZJYlS?3%a-CJ#js90hoh5W5d^OMwCFmpryHFr|mG+*ZP$ zqyS5BW@s}|3xUO0PR<^{a2M(gkP5BDGxvkWkPudSV*TMRK5Qm4?~VuqVAOerffRt$HGAvp;M++Iq$E6alB z;ykBr-eZ6v_H^1Wip56Czj&=`mb^TsX|FPN#-gnlP03AkiJDM=?y|LzER1M93R4sC z*HT(;EV=*F*>!+Z{r!KG?6ODMGvkt3viG=@kQJHNMYd}bS4KrrHf4`&*(0m0R5Hqz zEk)r=sFeS?MZRvn<@Z0&bDw)XkMnw+_xqgp=W{;ioX`6;G-P9N%wfoYJ$-m$L#MC% z^sH?tSzA|WWP(cN3({~_*X$l{M*;1V{l$;T6b){#l4pswDTid26HaXgKed}13YIP= zJRvA3nmx{}R$Lr&S4!kWU3`~dxM}>VXWu6Xd(VP}z1->h&f%82eXD_TuTs@=c;l0T z|LHmWKJ+?7hkY=YM>t}zvb4|lV;!ARMtWFp!E^J=Asu9w&kVF*i{T#}sY++-qnVh! z5TQ|=>)+vutf{&qB+LO9^jm#rD7E5+tcorr^Fn5Xb0B;)f^$7Ev#}G_`r==ea294V z--v4LwjswWlSq9ba6i?IXr8M_VEGQ$H%hCqJTFQ3+1B9tmxDUhnNU%dy4+zbqYJ|o z3!N{b?A@{;cG2~nb-`|z;gEDL5ffF@oc3`R{fGi)0wtMqEkw4tRX3t;LVS3-zAmg^ zgL7Z{hmdPSz9oA@t>tZ1<|Khn&Lp=_!Q=@a?k+t~H&3jN?dr(}7s;{L+jiKY57?WsFBfW^mu6a03_^VKrdK=9egXw@!nzZ3TbYc*osyQNoCXPYoFS<&Nr97MrQCOK(gO8 z;0@iqRTJy4-RH)PJld5`AJN}n?5r^-enKrHQOR;z>UMfm+e8~4ZL5k>oXMiYq12Bx4eVQv0jFgp_zC#``sjZpywYqISMP}VZ@!~1Mf$!x|opj%mQ98JnSk@`~ zPmmyuPZKtZOnEC!1y!?`TYRsZ!II;d!iln}%e}bk5qIiUADERr*K$3dekgHV9TtBX zi5q!J!6Zgd#cLxRmZN^J`o@Zv{+p+<_#8^nvY)44Hw_2i@?R&5n^q33fpOnDg1nPQ z_r<$hURl~OketX|Tdbvf_7=3x^rSFJtEp@tuDpVB&uq)qW;xUQ7mmkr-@eZwa$l+? zoKk``Vz@TH#>jMce*8>@FZ+@BEUdYa_K0i|{*;j9MW3K%pnM*T;@>|o@lMhgLrpZP5aol(z>g;b4}|e$U~Fn zGL%(}p%Jsl4LxE!VW_Y4T>e}W4e#~F03H_^R!Q)kpJG{lO!@I4{mFo^V#ayHh_5~o zB$O71gcE(G@6xv);#Ky?e(Ed}^O+Ho(t=93T9T3TnEY(OVf_dR-gY@jj+iJSY?q|6prBv(S9A4k=2fNZz!W@S=B@~b?TJRTuBQq448@juN#Y=3q=^VCF>Z}n6wICJ<^^Kn8C;mK zZYiFSN#Z$?NDGV7(#}q2tAZAtE63icK-MY>UQu4MWlGIbJ$AF8Zt-jV;@7P5MPI>% zPWvO!t%1+s>-A%`;0^o8Ezeaa4DMwI8ooQrJ;ax@Qt*6XONWw)dPwOPI9@u*EG&844*1~EoZ2qsAe~M>d`;Bc_CWY zMoDKEmDh-}k9d6*<0g@aQmsnrM1H9IcKYZs)><)d92{|0Hh8?~XbF)7U+UmP@Pw_6geVB?7N$4J4*E0z3EO&5kRS(EE zv92(+e5WxLXMN{h;-|8@!Q#0q247hb^3R%*k3MuMO5*L}$0D#5P*N$aHd54C+=_RToYXTyewugOaDmGsCvb4H1s=@gkfVnzTCWKMa-Mm1v4Wq!t-JIrbV&EWwKDe ze#kJpOq#iRlFz%5#6Fio9IUlKnQ#X&DY8Ux#<-WqxAac-y%U_L+EZZ4Rg5*yNg`f< zSZn&uio@zanUCPqX1l4W&B!;UWs#P7B^|4WwoCxQXl|44n^cBNqu=3Vl*ltAqsUQO z9q_@nD0zq0O8r`coEm>9+|rA3HL#l}X;0##>SJS$cVavOZVCpSGf4mUU1( zWaRCUYc^9QbG9=vpWo%xP}CMFnMb{reA`K7tT(t5DM)d9l}jVPY>qoRzT zE3m-p#=i=$9x*CB`AL>SY}u3agYFl#uULNen#&44H;!L@I{RI=PlWxG8J((f)ma7A z@jLvQ>?Nx`n?3ChRG#HqE3MXP8*o3!Qq`+t8EMt_p)oeKHqPusBxPn!#?R??-=e3e zo73WNs_IZF`WLigre=|`aS2^> zN1zn!7k&Dh28t%VpJ%**&E!eAcB5oLjQFFcJQj*URMia%Ya3@q1UQ18=oWMM6`I}iT_&L1gl?*~6nU4q4Z0`H<5yDp(HeZ+RGf9`mM&= zn-qRp%i!g$R;i1d1aMZ{IewNjE@p2+Z{`x{*xL*x$?WV~{BjJpsP&C&JK0HLoyf z`0z^v&fBQSa!I7FU~9MaQ%e|?RP>sM^2PL!mE^Q1Ig_4M$5BRfi72oMYu6Ke?wmDX z@0a%-V|z}b23K=ye(W+fG#w|jJUnT{=KR5jfuq!RX}<1irTDw(${<&}dWQu4;EuE< z@3u4dBkQaCHHM&;cE0z50_V!(vJ1_V)A8?C#eJuLkt!98Z%|Bgzidc0j|z(&o)TCzYlrgZA zC3@i>L!&Gw_~7`>puB97I2lK)lESZQqVXc_8T^G2O#VHhO?IC$g zOYhXJ7)~C<8l|Xrftka@QuowScM{K&0zskoU$Aw~vIRVRF9TEQ4*3=_5)98B`=t8(N%ZuWqmwlW zllAzq=E5_5!sKDXam@w`ZD(nl%LAPxQuEtDcKPqu9LPJvNIITawU#c^PQ2HmZgs)r zH^+gRwZ?0)8IFQgU)+p@0Iqb^tcEoqcB@zhfz_FaOM&_d<|jnU>q5nSKa<@%9|dje zIupcg1!tRiMP4X=oG<7s4|AW&^-Cw4FL9OuI$t zxjc*y;Uw!G7a|jz>E*2+PlR(CemWebS7m-&*CDwnmxbiRqJvQ&os-sC&4OWt^(2@vG4|jui#Df@-D= zh3D%8Y3R6+jRBStSvH9pt&tCI`NK08J1*pC(?OM0h!bS-JK3I}`pDY-fDIaB_*W6KS+TO0Q*%kkeuN6uWITt=TsCGw6uBE710q; zRluI%j{?@jwhM|l5&TB!-TkQs!A=DXRE>u18t@;zndD0M$U@Igrt?UW2; z7%=dsHIVH_LCkGUU0fW&UMjDnvjcc0Mp(mK&;d~ZJ5EJ)#7@aTZvGDFXzFZg2Lq~s z5PR_LazNN)JD5K_uK*Hy{mXuHTkGGv|9V8KP#iQ$3!G*^>7UiE{|1G1A-qg(xH;Xa>&%f|BZkH zG=J^0pHzSAqv5*5ysQ{Puy^-_|IPrii zKS$mE10Zngf>Sgg@BjpRyJbrHeo zD8Ro0LI*W#+9?^xlOS^c>Z^^n^0I|FH^@^`ZR`{H=$ zjO0_$cnpBM7Zcm?H_RXIu-Lu~qweDSV|tEZBZh!e6hQy->}e;d#osZ1hQj{HhHkC0 zJ|F-HKmeTGgDe979ogBz24;@<|I7;TU!IXb@oWMsMECIETmQy`zPtM`|NP}PjzR_u zKMG1Z{%1kWeMfEf(10U#w!clmQ2)JC8zm(Fv!H4dUHQHCFLikID?hrd{0>kCQt?kP zdqn2ZG0}ytcQJ7t_B3s0ZvH3PYjkjQ`Q%;jV@?MK-+z3etBCGGo4f4`y^|AdCs!DH zThTQ;cL5dM{|tB_1y6K3bVa^hx_<9J(}5`2SDz1^0bT!Vm*JV;9~t&{IC{$DUAVV* z{|E=#yN{wNdTY@$6z{_KNA3&%w|vFu1n9XRcM0Ak>`UW!lQ`ah3D4r%}Z 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" \ From e71be1ed64175637ecce2935ee102098b74ea8bc Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Wed, 11 Dec 2024 10:52:14 +0100 Subject: [PATCH 09/34] Fix(folder provider): display pojav version This allows the sort to be stable when listing pojav versions in the file app. --- .../java/net/kdt/pojavlaunch/scoped/FolderProvider.java | 8 +++++++- app_pojavlauncher/src/main/res/values/strings.xml | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) 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..5bd56abeef 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 @@ -16,6 +16,7 @@ import androidx.annotation.Nullable; +import net.kdt.pojavlaunch.BuildConfig; import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; @@ -76,10 +77,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); diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index cfc5affce1..91104432dc 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -323,6 +323,7 @@ Install Apply + Debug No modpacks found Failed to find modpacks From 011139db38361334fd5ffc2f9096b557e01f5db2 Mon Sep 17 00:00:00 2001 From: artdeell Date: Thu, 12 Dec 2024 12:32:18 +0300 Subject: [PATCH 10/34] Feat[folder_provider]: notify the file manager on file changes --- .../pojavlaunch/scoped/FolderProvider.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 5bd56abeef..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; @@ -47,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. @@ -97,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; } @@ -110,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; } @@ -129,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; } @@ -152,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(); } @@ -196,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 @@ -338,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 From 89f0254106d3f83284d3ae229b0e938d144fe860 Mon Sep 17 00:00:00 2001 From: TarikBR <64391349+TarikBR@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:09:44 -0300 Subject: [PATCH 11/34] Fix[docs]: Change license to LGPLv3 in readme and fix link (#6389) Closes #6384 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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! From e0fcb1e67d82cb6dbf2ed62765d81093cae591db Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Mon, 16 Dec 2024 16:04:59 +0100 Subject: [PATCH 12/34] Qol(settings): use energy saving settings for powerful devices Less boiling pocket heaters. May contain a small few refactor --- .../customcontrols/gamepad/Gamepad.java | 12 ++-- .../prefs/LauncherPreferences.java | 62 ++++++++++++------- .../prefs/QuickSettingSideDialog.java | 8 +-- .../LauncherPreferenceVideoFragment.java | 21 ++++--- .../kdt/pojavlaunch/utils/LocaleUtils.java | 3 +- .../src/main/res/values/values.xml | 11 ++++ .../src/main/res/xml/pref_control.xml | 12 ++-- .../src/main/res/xml/pref_java.xml | 2 +- .../src/main/res/xml/pref_video.xml | 2 +- 9 files changed, 81 insertions(+), 52 deletions(-) create mode 100644 app_pojavlauncher/src/main/res/values/values.xml 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/prefs/LauncherPreferences.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java index 5e73275034..9eba503313 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,13 +10,9 @@ 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; @@ -28,11 +24,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 +45,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 +70,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 +85,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 +119,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 +151,33 @@ 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 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; + return true; + } + /** 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..2da6d8047b 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 @@ -123,7 +123,7 @@ private void setupListeners() { }); mGyroSensitivityBar.setRange(25, 300); - mGyroSensitivityBar.setIncrement(5); + mGyroSensitivityBar.setIncrement(mGyroSensitivityBar.getContext().getResources().getInteger(R.integer.gyro_speed_seekbar_increment)); mGyroSensitivityBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> { PREF_GYRO_SENSITIVITY = progress / 100f; mEditor.putInt("gyroSensitivity", progress); @@ -133,7 +133,7 @@ private void setupListeners() { setSeekTextPercent(mGyroSensitivityText, mGyroSensitivityBar.getProgress()); mMouseSpeedBar.setRange(25, 300); - mMouseSpeedBar.setIncrement(5); + mMouseSpeedBar.setIncrement(mMouseSpeedBar.getContext().getResources().getInteger(R.integer.mouse_speed_seekbar_increment)); mMouseSpeedBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> { PREF_MOUSESPEED = progress / 100f; mEditor.putInt("mousespeed", progress); @@ -143,7 +143,7 @@ private void setupListeners() { setSeekTextPercent(mMouseSpeedText, mMouseSpeedBar.getProgress()); mGestureDelayBar.setRange(100, 1000); - mGestureDelayBar.setIncrement(10); + mGestureDelayBar.setIncrement(mGestureDelayBar.getContext().getResources().getInteger(R.integer.gesture_delay_seekbar_increment)); mGestureDelayBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> { PREF_LONGPRESS_TRIGGER = progress; mEditor.putInt("timeLongPressTrigger", progress); @@ -153,7 +153,7 @@ private void setupListeners() { setSeekTextMillisecond(mGestureDelayText, mGestureDelayBar.getProgress()); mResolutionBar.setRange(25, 100); - mResolutionBar.setIncrement(5); + mResolutionBar.setIncrement(mResolutionBar.getContext().getResources().getInteger(R.integer.resolution_seekbar_increment)); 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/LauncherPreferenceVideoFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceVideoFragment.java index 6876f2aa45..4b2bc5279b 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,31 @@ 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.setMin(25); + 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/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/res/values/values.xml b/app_pojavlauncher/src/main/res/values/values.xml new file mode 100644 index 0000000000..af6989ff31 --- /dev/null +++ b/app_pojavlauncher/src/main/res/values/values.xml @@ -0,0 +1,11 @@ + + + 5 + 10 + 5 + 5 + 5 + 5 + 5 + 8 + \ 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..e892adcfad 100644 --- a/app_pojavlauncher/src/main/res/xml/pref_control.xml +++ b/app_pojavlauncher/src/main/res/xml/pref_control.xml @@ -35,7 +35,7 @@ android:title="@string/mcl_setting_title_longpresstrigger" app2:showSeekBarValue="true" app2:selectable="false" - app2:seekBarIncrement="10" + app2:seekBarIncrement="@integer/gesture_delay_seekbar_increment" android:icon="@drawable/ic_setting_gesture_time" /> @@ -50,7 +50,7 @@ android:summary="@string/mcl_setting_subtitle_buttonscale" app2:showSeekBarValue="true" app2:selectable="false" - app2:seekBarIncrement="5" + app2:seekBarIncrement="@integer/button_scale_seekbar_increment" android:icon="@drawable/ic_setting_control_scale" /> @@ -82,7 +82,7 @@ android:title="@string/mcl_setting_title_mousespeed" android:icon="@drawable/ic_setting_mouse_speed" app2:selectable="false" - app2:seekBarIncrement="5" + 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 46616ae4d8..239836cd7d 100644 --- a/app_pojavlauncher/src/main/res/xml/pref_java.xml +++ b/app_pojavlauncher/src/main/res/xml/pref_java.xml @@ -25,7 +25,7 @@ android:summary="@string/mcl_memory_allocation_subtitle" android:title="@string/mcl_memory_allocation" app2:showSeekBarValue="true" - app2:seekBarIncrement="8" + app2:seekBarIncrement="@integer/memory_seekbar_increment" app2:selectable="false"/> From 4738e1aa59b85f2b2e4eb972fcc2f22382e00abd Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Thu, 19 Dec 2024 21:39:38 +0100 Subject: [PATCH 13/34] Tweak(pref-detection): Verify all cores aren't the same frequency --- .../pojavlaunch/prefs/LauncherPreferences.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 9eba503313..5f26453f1b 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 @@ -17,6 +17,8 @@ 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"; @@ -175,9 +177,22 @@ private static boolean isDevicePowerful(Context context) { 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; From 38e47687fad162b4797e5f158bc4d1e49e3877fe Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Fri, 20 Dec 2024 22:35:13 +0100 Subject: [PATCH 14/34] Tweak(jvm arg): take control of -XX:ActiveProcessorCount Setting it ourselves guarantee a better experience by default --- .../java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java | 2 +- .../src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) 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 5f26453f1b..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 @@ -155,7 +155,7 @@ private static int findBestRAMAllocation(Context ctx){ /// Find a correct resolution for the device /// - /// Some devices are shipped with ridiculously high resolution, which can cause performance issues + /// 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(); 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..521c91de84 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 @@ -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); From b3df2645e570e6cbfc937aca046a66ff46b71ea0 Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Fri, 20 Dec 2024 22:36:53 +0100 Subject: [PATCH 15/34] Fix,refactor(profiles): prefer nameless profiles Also refactored the use of a constant across the code --- .../pojavlaunch/profiles/ProfileAdapter.java | 30 +++++++++---------- .../tasks/AsyncMinecraftDownloader.java | 5 ++-- .../launcherprofiles/MinecraftProfile.java | 7 +++-- 3 files changed, 23 insertions(+), 19 deletions(-) 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/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/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; } From dcbb1e010e8aac82911b47dde6e5ee6b9ac16fae Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Mon, 23 Dec 2024 09:36:59 +0100 Subject: [PATCH 16/34] refactor(customseekbar): implement seekBarIncrement attribute --- .../src/main/java/com/kdt/CustomSeekbar.java | 19 ++++++++++++++----- .../prefs/QuickSettingSideDialog.java | 4 ---- .../main/res/layout/dialog_quick_setting.xml | 8 ++++++++ .../src/main/res/values/attributes.xml | 7 +++++++ 4 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 app_pojavlauncher/src/main/res/values/attributes.xml diff --git a/app_pojavlauncher/src/main/java/com/kdt/CustomSeekbar.java b/app_pojavlauncher/src/main/java/com/kdt/CustomSeekbar.java index 050276e3e2..be80f33a88 100644 --- a/app_pojavlauncher/src/main/java/com/kdt/CustomSeekbar.java +++ b/app_pojavlauncher/src/main/java/com/kdt/CustomSeekbar.java @@ -2,9 +2,14 @@ import android.annotation.SuppressLint; import android.content.Context; +import android.content.res.TypedArray; 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 */ @@ -19,22 +24,22 @@ public class CustomSeekbar extends SeekBar { 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(); + setup(attrs); } public CustomSeekbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - setup(); + setup(attrs); } public void setIncrement(int increment) { @@ -75,7 +80,11 @@ public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) { mListener = l; } - public void setup() { + 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; 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 2da6d8047b..3cf0131dfd 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 @@ -123,7 +123,6 @@ private void setupListeners() { }); mGyroSensitivityBar.setRange(25, 300); - mGyroSensitivityBar.setIncrement(mGyroSensitivityBar.getContext().getResources().getInteger(R.integer.gyro_speed_seekbar_increment)); mGyroSensitivityBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> { PREF_GYRO_SENSITIVITY = progress / 100f; mEditor.putInt("gyroSensitivity", progress); @@ -133,7 +132,6 @@ private void setupListeners() { setSeekTextPercent(mGyroSensitivityText, mGyroSensitivityBar.getProgress()); mMouseSpeedBar.setRange(25, 300); - mMouseSpeedBar.setIncrement(mMouseSpeedBar.getContext().getResources().getInteger(R.integer.mouse_speed_seekbar_increment)); mMouseSpeedBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> { PREF_MOUSESPEED = progress / 100f; mEditor.putInt("mousespeed", progress); @@ -143,7 +141,6 @@ private void setupListeners() { setSeekTextPercent(mMouseSpeedText, mMouseSpeedBar.getProgress()); mGestureDelayBar.setRange(100, 1000); - mGestureDelayBar.setIncrement(mGestureDelayBar.getContext().getResources().getInteger(R.integer.gesture_delay_seekbar_increment)); mGestureDelayBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> { PREF_LONGPRESS_TRIGGER = progress; mEditor.putInt("timeLongPressTrigger", progress); @@ -153,7 +150,6 @@ private void setupListeners() { setSeekTextMillisecond(mGestureDelayText, mGestureDelayBar.getProgress()); mResolutionBar.setRange(25, 100); - mResolutionBar.setIncrement(mResolutionBar.getContext().getResources().getInteger(R.integer.resolution_seekbar_increment)); mResolutionBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> { PREF_SCALE_FACTOR = progress/100f; mEditor.putInt("resolutionRatio", progress); 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..211e63dd7e 100644 --- a/app_pojavlauncher/src/main/res/layout/dialog_quick_setting.xml +++ b/app_pojavlauncher/src/main/res/layout/dialog_quick_setting.xml @@ -27,6 +27,8 @@ android:layout_width="0dp" android:layout_height="@dimen/_36sdp" + app:seekBarIncrement="@integer/resolution_seekbar_increment" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.538" app:layout_constraintStart_toStartOf="parent" @@ -92,6 +94,8 @@ android:layout_width="0dp" android:layout_height="@dimen/_36sdp" + app:seekBarIncrement="@integer/gyro_speed_seekbar_increment" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.538" app:layout_constraintStart_toStartOf="parent" @@ -127,6 +131,8 @@ android:layout_width="0dp" android:layout_height="@dimen/_36sdp" + app:seekBarIncrement="@integer/mouse_speed_seekbar_increment" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.538" app:layout_constraintStart_toStartOf="parent" @@ -172,6 +178,8 @@ android:layout_width="0dp" android:layout_height="@dimen/_36sdp" + app:seekBarIncrement="@integer/gesture_delay_seekbar_increment" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.538" app:layout_constraintStart_toStartOf="parent" 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..30cbe7a162 --- /dev/null +++ b/app_pojavlauncher/src/main/res/values/attributes.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file From 8cc5cca99fa751be681deaded2f911955ac21c1d Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Mon, 23 Dec 2024 22:05:55 +0100 Subject: [PATCH 17/34] Fix(quick-setting): improper placement This was due to an issue that only happens within older android versions. --- .../src/main/java/net/kdt/pojavlaunch/MainActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 92a25e9887..bfabd8f368 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java @@ -243,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(); }); From f2e95b6e6595bfe5c9a3e941a23854b7b4bf9ed4 Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Tue, 24 Dec 2024 23:51:42 +0100 Subject: [PATCH 18/34] refactor(seekbars):migrate range values into xml files --- .../src/main/java/com/kdt/CustomSeekbar.java | 125 ++++++++++-------- .../main/java/net/kdt/pojavlaunch/Tools.java | 39 +++++- .../prefs/CustomSeekBarPreference.java | 8 +- .../prefs/QuickSettingSideDialog.java | 4 - .../LauncherPreferenceControlFragment.java | 7 - .../LauncherPreferenceJavaFragment.java | 11 +- .../LauncherPreferenceVideoFragment.java | 1 - .../main/res/layout/dialog_quick_setting.xml | 26 ++-- .../src/main/res/values/attributes.xml | 2 + .../src/main/res/values/values.xml | 26 ++++ .../src/main/res/xml/pref_control.xml | 14 ++ .../src/main/res/xml/pref_java.xml | 1 + .../src/main/res/xml/pref_video.xml | 1 + 13 files changed, 172 insertions(+), 93 deletions(-) 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..641f1d5b4a 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; @@ -538,7 +538,7 @@ private static void setFullscreenLegacy(Activity activity, boolean fullscreen) { 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) { WindowInsetsControllerCompat windowInsetsController = WindowCompat.getInsetsController(activity.getWindow(), activity.getWindow().getDecorView()); @@ -555,25 +555,31 @@ 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) { + setFullscreenSdk30(activity, fullscreen); + /* if (SDK_INT >= Build.VERSION_CODES.R) { - setFullscreenSdk30(activity, fullscreen); }else { setFullscreenLegacy(activity, fullscreen); - } + }*/ } public static DisplayMetrics currentDisplayMetrics; @@ -1343,4 +1349,23 @@ public static void dialogForceClose(Context ctx) { } }).show(); } + + public static void setThreadsPriority(int priority) { + Process.getThreadPriority(Process.myTid()); + + + + Map threads = Thread.getAllStackTraces(); + for (Thread thread : threads.keySet()) { + //Log.d("Tools, thread: ", thread.getName()); + Log.d("Tools, thread: ", thread + " group: " + thread.getThreadGroup()); + Log.d("Tools, thread: ", Arrays.toString(thread.getStackTrace())); + Log.d("Tools, thread: ", String.valueOf(thread.getState())); + try { + thread.setPriority(priority); + }catch (Exception e) { + Log.e("Tools: thread", "Failed to set priority", 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/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/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" /> From 9a7fb2ae69fa500ccb8acbbbcb5f8a471428ddbc Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Wed, 25 Dec 2024 19:40:20 +0100 Subject: [PATCH 19/34] cleanup(tools): remove unused code related to fullscreen --- .../main/java/net/kdt/pojavlaunch/Tools.java | 37 +------------------ 1 file changed, 1 insertion(+), 36 deletions(-) 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 641f1d5b4a..5e20e9b839 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -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 - } - - - 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) { @@ -573,15 +547,6 @@ private static void setFullscreenSdk30(Activity activity, boolean fullscreen) { } - public static void setFullscreen(Activity activity, boolean fullscreen) { - setFullscreenSdk30(activity, fullscreen); - /* - if (SDK_INT >= Build.VERSION_CODES.R) { - }else { - setFullscreenLegacy(activity, fullscreen); - }*/ - } - public static DisplayMetrics currentDisplayMetrics; public static void updateWindowSize(Activity activity) { From 99c8ea2bfdd0b8e9ea3251865c38bc98055398d0 Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Wed, 25 Dec 2024 19:45:33 +0100 Subject: [PATCH 20/34] cleanup(tools): remove unused code --- .../main/java/net/kdt/pojavlaunch/Tools.java | 19 ------------------- 1 file changed, 19 deletions(-) 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 5e20e9b839..316dc08220 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -1314,23 +1314,4 @@ public static void dialogForceClose(Context ctx) { } }).show(); } - - public static void setThreadsPriority(int priority) { - Process.getThreadPriority(Process.myTid()); - - - - Map threads = Thread.getAllStackTraces(); - for (Thread thread : threads.keySet()) { - //Log.d("Tools, thread: ", thread.getName()); - Log.d("Tools, thread: ", thread + " group: " + thread.getThreadGroup()); - Log.d("Tools, thread: ", Arrays.toString(thread.getStackTrace())); - Log.d("Tools, thread: ", String.valueOf(thread.getState())); - try { - thread.setPriority(priority); - }catch (Exception e) { - Log.e("Tools: thread", "Failed to set priority", e); - } - } - } } From 5d80d9baecd3892dd1bced2a46a2fb3c8b64d8f5 Mon Sep 17 00:00:00 2001 From: Maksim Belov <45949002+artdeell@users.noreply.github.com> Date: Mon, 30 Dec 2024 20:31:59 +0300 Subject: [PATCH 21/34] Feat[downloader]: downloader improvements (#6428) - Add download size queries through a HEAD request - Use the file size for progress instead of file count when all file size are available - Add download speed meter --- .../pojavlaunch/mirrors/DownloadMirror.java | 23 ++++++- .../tasks/MinecraftDownloader.java | 68 ++++++++++++++----- .../pojavlaunch/tasks/SpeedCalculator.java | 44 ++++++++++++ .../kdt/pojavlaunch/utils/DownloadUtils.java | 17 +++++ .../src/main/res/values/strings.xml | 3 +- 5 files changed, 136 insertions(+), 19 deletions(-) create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/SpeedCalculator.java 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/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/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 From a77271df03d38351507a9139c8a3d6b35cf917ea Mon Sep 17 00:00:00 2001 From: movte <212824502@qq.com> Date: Tue, 17 Dec 2024 21:27:18 +0800 Subject: [PATCH 22/34] Fix the issue where the long-press trigger delay does not refresh properly. --- .../customcontrols/mouse/LeftClickGesture.java | 7 ++++++- .../customcontrols/mouse/RightClickGesture.java | 9 +++++++-- .../customcontrols/mouse/ValidatorGesture.java | 12 +++++++----- 3 files changed, 20 insertions(+), 8 deletions(-) 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..ef93c222bb 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 getDelayValue() { + 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..39dfa965fb 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,12 +6,12 @@ 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() { @@ -29,6 +29,11 @@ public void setMotion(float deltaX, float deltaY) { mGestureEndY += deltaY; } + @Override + protected int getDelayValue() { + 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. 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..c43817e05b 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, getDelayValue()); mGestureActive = true; return true; } @@ -54,6 +51,11 @@ public final void run() { onGestureCancelled(false); } + /** + * @return the duration after which the class will call checkAndTrigger(). + */ + protected abstract int getDelayValue(); + /** * This method will be called after mRequiredDuration milliseconds, if the gesture was not cancelled. * @return false if you want to mark this gesture as "inactive" From 93924b429b7213e78c41f69350a3d6ce4dd69289 Mon Sep 17 00:00:00 2001 From: artdeell Date: Thu, 2 Jan 2025 18:53:35 +0300 Subject: [PATCH 23/34] Fix[gestures]: rename method and improve documentation --- .../customcontrols/mouse/LeftClickGesture.java | 2 +- .../customcontrols/mouse/RightClickGesture.java | 2 +- .../customcontrols/mouse/ValidatorGesture.java | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) 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 ef93c222bb..9a4c20b447 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 @@ -28,7 +28,7 @@ public final void inputEvent() { } @Override - protected int getDelayValue() { + protected int getCheckDuration() { return LauncherPreferences.PREF_LONGPRESS_TRIGGER; } 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 39dfa965fb..158634b629 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 @@ -30,7 +30,7 @@ public void setMotion(float deltaX, float deltaY) { } @Override - protected int getDelayValue() { + protected int getCheckDuration() { return 150; } 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 c43817e05b..5ed9da5f36 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 @@ -25,7 +25,7 @@ public ValidatorGesture(Handler mHandler) { */ public final boolean submit() { if(mGestureActive) return false; - mHandler.postDelayed(this, getDelayValue()); + mHandler.postDelayed(this, getCheckDuration()); mGestureActive = true; return true; } @@ -52,12 +52,13 @@ public final void run() { } /** - * @return the duration after which the class will call checkAndTrigger(). + * 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 getDelayValue(); + protected abstract int getCheckDuration(); /** - * This method will be called after mRequiredDuration milliseconds, if the gesture was not cancelled. + * This method will be called after getCheckDuration() milliseconds, if the gesture was not cancelled. * @return false if you want to mark this gesture as "inactive" * true otherwise */ From dd15d8a46960b1bd61c803d032abad74853b196c Mon Sep 17 00:00:00 2001 From: artdeell Date: Thu, 2 Jan 2025 21:34:25 +0300 Subject: [PATCH 24/34] Style[gestures]: rename method to getGestureDelay --- .../pojavlaunch/customcontrols/mouse/LeftClickGesture.java | 2 +- .../pojavlaunch/customcontrols/mouse/RightClickGesture.java | 2 +- .../pojavlaunch/customcontrols/mouse/ValidatorGesture.java | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) 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 9a4c20b447..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 @@ -28,7 +28,7 @@ public final void inputEvent() { } @Override - protected int getCheckDuration() { + protected int getGestureDelay() { return LauncherPreferences.PREF_LONGPRESS_TRIGGER; } 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 158634b629..3ca4c02ea3 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 @@ -30,7 +30,7 @@ public void setMotion(float deltaX, float deltaY) { } @Override - protected int getCheckDuration() { + protected int getGestureDelay() { return 150; } 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 5ed9da5f36..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 @@ -25,7 +25,7 @@ public ValidatorGesture(Handler mHandler) { */ public final boolean submit() { if(mGestureActive) return false; - mHandler.postDelayed(this, getCheckDuration()); + mHandler.postDelayed(this, getGestureDelay()); mGestureActive = true; return true; } @@ -55,10 +55,10 @@ public final void run() { * 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 getCheckDuration(); + protected abstract int getGestureDelay(); /** - * This method will be called after getCheckDuration() milliseconds, if the gesture was not cancelled. + * 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 */ From 80e0a6aac8ef542cac1c65bb6f10c8eb9373dc2c Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 2 Jan 2025 15:00:03 -0600 Subject: [PATCH 25/34] Update GPLAY_PRIVACY_POLICY (#6174) Update GPLAY_PRIVACY_POLICY to be more clear, and remove the news entry as the news in Pojav no longer exists. --- GPLAY_PRIVACY_POLICY | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 2e596cd07f1ec9d3f774f1239949db8b7e13e3e8 Mon Sep 17 00:00:00 2001 From: Maksim Belov <45949002+artdeell@users.noreply.github.com> Date: Sat, 4 Jan 2025 02:41:02 +0300 Subject: [PATCH 26/34] Fix[renderer]: move GL4ES initialization code (#6447) --- .../src/main/jni/ctxbridges/gl_bridge.c | 40 ++++++++----------- .../java/org/lwjgl/opengl/GLCapabilities.java | 2 + .../org/lwjgl/opengl/PojavRendererInit.java | 39 ++++++++++++++++++ 3 files changed, 58 insertions(+), 23 deletions(-) create mode 100644 jre_lwjgl3glfw/src/main/java/org/lwjgl/opengl/PojavRendererInit.java 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/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); +} From a72451c9e7735df2d7000358489311784c896697 Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Tue, 7 Jan 2025 20:21:59 +0100 Subject: [PATCH 27/34] fix(gesture): Right click not working Turns out if you were perfectly still, it would fail. For a quick tap, it was fairly easy to create. --- .../pojavlaunch/customcontrols/mouse/RightClickGesture.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 3ca4c02ea3..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 @@ -17,14 +17,15 @@ public RightClickGesture(Handler 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; } @@ -49,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); From 041838725b1c5938c7611fa465a3e6959caccea7 Mon Sep 17 00:00:00 2001 From: artdeell Date: Sun, 5 Jan 2025 12:05:09 +0300 Subject: [PATCH 28/34] Fix[ffmpeg_plugin]: better FFmpeg plugin insertion NOTE: requires a different version of the FFmpeg plugin which I haven't made yet --- .../kdt/pojavlaunch/plugins/FFmpegPlugin.java | 8 ++++- .../net/kdt/pojavlaunch/utils/JREUtils.java | 2 +- .../src/main/jni/input_bridge_v3.c | 35 +++++++++++++------ 3 files changed, 33 insertions(+), 12 deletions(-) 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/utils/JREUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java index 521c91de84..dde80ec37c 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) { diff --git a/app_pojavlauncher/src/main/jni/input_bridge_v3.c b/app_pojavlauncher/src/main/jni/input_bridge_v3.c index 03bb79f627..ce106f859c 100644 --- a/app_pojavlauncher/src/main/jni/input_bridge_v3.c +++ b/app_pojavlauncher/src/main/jni/input_bridge_v3.c @@ -230,23 +230,38 @@ void sendData(int type, int i1, int i2, int i3, int i4) { atomic_fetch_add_explicit(&pojav_environ->eventCounter, 1, memory_order_acquire); } +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; +} + /** * Hooked version of java.lang.UNIXProcess.forkAndExec() - * which is used to handle the "open" command. + * which is used to handle the "open" command and "ffmpeg" invocations */ 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); - } + 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); - Java_org_lwjgl_glfw_CallbackBridge_nativeClipboard(env, NULL, /* CLIPBOARD_OPEN */ 2002, argBlock); - return 0; + if(strcmp(prog_basename, "xdg-open") == 0) { + // When invoking xdg-open, send that open command into the android half instead + Java_org_lwjgl_glfw_CallbackBridge_nativeClipboard(env, NULL, /* CLIPBOARD_OPEN */ 2002, 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. + const char* ffmpeg_path = getenv("POJAV_FFMPEG_PATH"); + if(ffmpeg_path != NULL) { + prog = stringToBytes(env, ffmpeg_path); + } + } + return orig_ProcessImpl_forkAndExec(env, process, mode, helperpath, prog, argBlock, argc, envBlock, envc, dir, std_fds, redirectErrorStream); } void hookExec() { From 6d39ab2d49696d3f5a00de06f0d9ff0b8b24bbf4 Mon Sep 17 00:00:00 2001 From: Maksim Belov Date: Sun, 5 Jan 2025 19:03:01 +0300 Subject: [PATCH 29/34] Fix[ffmpeg_plugin]: replace LD_LIBRARY_PATH/PATH for ffmpeg, switch default exec mode --- .../net/kdt/pojavlaunch/utils/JREUtils.java | 3 +- app_pojavlauncher/src/main/jni/Android.mk | 1 + .../src/main/jni/input_bridge_v3.c | 52 ----------- .../src/main/jni/java_exec_hooks.c | 90 +++++++++++++++++++ 4 files changed, 93 insertions(+), 53 deletions(-) create mode 100644 app_pojavlauncher/src/main/jni/java_exec_hooks.c 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 dde80ec37c..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 @@ -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..9ee43d7e75 100644 --- a/app_pojavlauncher/src/main/jni/Android.mk +++ b/app_pojavlauncher/src/main/jni/Android.mk @@ -45,6 +45,7 @@ LOCAL_SRC_FILES := \ jre_launcher.c \ utils.c \ stdio_is.c \ + java_exec_hooks.c \ driver_helper/nsbypass.c ifeq ($(TARGET_ARCH_ABI),arm64-v8a) diff --git a/app_pojavlauncher/src/main/jni/input_bridge_v3.c b/app_pojavlauncher/src/main/jni/input_bridge_v3.c index ce106f859c..951050a093 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) { @@ -230,56 +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); } -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; -} - -/** - * Hooked version of java.lang.UNIXProcess.forkAndExec() - * which is used to handle the "open" command and "ffmpeg" invocations - */ -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 that open command into the android half instead - Java_org_lwjgl_glfw_CallbackBridge_nativeClipboard(env, NULL, /* CLIPBOARD_OPEN */ 2002, 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. - const char* ffmpeg_path = getenv("POJAV_FFMPEG_PATH"); - if(ffmpeg_path != NULL) { - prog = stringToBytes(env, ffmpeg_path); - } - } - return orig_ProcessImpl_forkAndExec(env, process, mode, helperpath, prog, argBlock, argc, envBlock, envc, dir, std_fds, redirectErrorStream); -} - -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 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..d0755a19b3 --- /dev/null +++ b/app_pojavlauncher/src/main/jni/java_exec_hooks.c @@ -0,0 +1,90 @@ +// +// 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 */ 2002, 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"); + prog = NULL; + 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 From aadb91dc989225f84e319f48dbb0fee267aecc59 Mon Sep 17 00:00:00 2001 From: Maksim Belov Date: Sun, 5 Jan 2025 19:11:30 +0300 Subject: [PATCH 30/34] Whoops[exec_hooks]: do not null program unconditionally --- app_pojavlauncher/src/main/jni/java_exec_hooks.c | 1 - 1 file changed, 1 deletion(-) diff --git a/app_pojavlauncher/src/main/jni/java_exec_hooks.c b/app_pojavlauncher/src/main/jni/java_exec_hooks.c index d0755a19b3..0972b3c73d 100644 --- a/app_pojavlauncher/src/main/jni/java_exec_hooks.c +++ b/app_pojavlauncher/src/main/jni/java_exec_hooks.c @@ -63,7 +63,6 @@ static jint hooked_ProcessImpl_forkAndExec(JNIEnv *env, jobject process, jint mo // 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"); - prog = NULL; if(ffmpeg_path != NULL) { replaceLibPathInEnvBlock(env, &envBlock, &envc, dirname(ffmpeg_path)); prog = stringToBytes(env, ffmpeg_path); From ebe5314f28df7d94c76384cb6ec463ff505ad0a0 Mon Sep 17 00:00:00 2001 From: Maksim Belov Date: Mon, 6 Jan 2025 20:32:54 +0300 Subject: [PATCH 31/34] Feat[lwjgl]: add vulkan to lwjgl dlopen hook, move hook to new file --- app_pojavlauncher/src/main/jni/Android.mk | 1 + app_pojavlauncher/src/main/jni/egl_bridge.c | 14 ++-- .../src/main/jni/input_bridge_v3.c | 41 ------------ .../src/main/jni/lwjgl_dlopen_hook.c | 64 +++++++++++++++++++ 4 files changed, 74 insertions(+), 46 deletions(-) create mode 100644 app_pojavlauncher/src/main/jni/lwjgl_dlopen_hook.c diff --git a/app_pojavlauncher/src/main/jni/Android.mk b/app_pojavlauncher/src/main/jni/Android.mk index 9ee43d7e75..a820a902a8 100644 --- a/app_pojavlauncher/src/main/jni/Android.mk +++ b/app_pojavlauncher/src/main/jni/Android.mk @@ -46,6 +46,7 @@ LOCAL_SRC_FILES := \ 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/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 951050a093..e1d406fa8c 100644 --- a/app_pojavlauncher/src/main/jni/input_bridge_v3.c +++ b/app_pojavlauncher/src/main/jni/input_bridge_v3.c @@ -228,47 +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); } -/** - * 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/lwjgl_dlopen_hook.c b/app_pojavlauncher/src/main/jni/lwjgl_dlopen_hook.c new file mode 100644 index 0000000000..96c6333b47 --- /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 installLinkerBugMitigation() { + __android_log_print(ANDROID_LOG_INFO, "LwjglLinkerHook", "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, "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 From 208b92be83b5b1202070335a94dacbad13e5bb14 Mon Sep 17 00:00:00 2001 From: Maksim Belov Date: Wed, 8 Jan 2025 00:00:48 +0300 Subject: [PATCH 32/34] Style[exec_hooks]: change function name, log lines, add clipboard constants --- app_pojavlauncher/src/main/jni/input_bridge_v3.c | 2 +- app_pojavlauncher/src/main/jni/java_exec_hooks.c | 2 +- app_pojavlauncher/src/main/jni/lwjgl_dlopen_hook.c | 4 ++-- app_pojavlauncher/src/main/jni/utils.h | 6 ++++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app_pojavlauncher/src/main/jni/input_bridge_v3.c b/app_pojavlauncher/src/main/jni/input_bridge_v3.c index e1d406fa8c..93c1b9041b 100644 --- a/app_pojavlauncher/src/main/jni/input_bridge_v3.c +++ b/app_pojavlauncher/src/main/jni/input_bridge_v3.c @@ -57,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(); } diff --git a/app_pojavlauncher/src/main/jni/java_exec_hooks.c b/app_pojavlauncher/src/main/jni/java_exec_hooks.c index 0972b3c73d..4469ba117d 100644 --- a/app_pojavlauncher/src/main/jni/java_exec_hooks.c +++ b/app_pojavlauncher/src/main/jni/java_exec_hooks.c @@ -52,7 +52,7 @@ static jint hooked_ProcessImpl_forkAndExec(JNIEnv *env, jobject process, jint mo 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 */ 2002, argBlock); + 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. diff --git a/app_pojavlauncher/src/main/jni/lwjgl_dlopen_hook.c b/app_pojavlauncher/src/main/jni/lwjgl_dlopen_hook.c index 96c6333b47..8694613d14 100644 --- a/app_pojavlauncher/src/main/jni/lwjgl_dlopen_hook.c +++ b/app_pojavlauncher/src/main/jni/lwjgl_dlopen_hook.c @@ -45,8 +45,8 @@ static jlong ndlopen_bugfix(__attribute__((unused)) JNIEnv *env, /** * Install the LWJGL dlopen hook. This allows us to mitigate linker bugs and add custom library overrides. */ -void installLinkerBugMitigation() { - __android_log_print(ANDROID_LOG_INFO, "LwjglLinkerHook", "API < 30 detected, installing linker bug mitigation"); +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) { 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); From 3c616b9d90c94afcfb40b5e2e5ff4deb67221b06 Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Mon, 13 Jan 2025 00:25:15 +0100 Subject: [PATCH 33/34] tweak(ui): inline the delete profile button Makes the main ui slightly cleaner --- .../java/com/kdt/mcgui/mcAccountSpinner.java | 59 ++++++++++++++----- .../net/kdt/pojavlaunch/LauncherActivity.java | 11 +--- .../layout-land/activity_pojav_launcher.xml | 10 ---- .../res/layout/activity_pojav_launcher.xml | 13 ---- .../res/layout/item_minecraft_account.xml | 40 ++++++++++--- 5 files changed, 77 insertions(+), 56 deletions(-) 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 { + private class AccountAdapter extends ArrayAdapter { private final HashMap 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/LauncherActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java index 00cc8e53be..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 mLaunchGameListener = (key, value) -> { if(mProgressLayout.hasProcesses()){ Toast.makeText(this, R.string.tasks_ongoing, Toast.LENGTH_LONG).show(); @@ -200,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); @@ -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/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" /> - - - - tools:text="HELLO THERE" - tools:drawableStart="@mipmap/ic_launcher" + + 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" /> + + + + + From ed89b44d3b7d476011d8d3e97300f9e309bd9e07 Mon Sep 17 00:00:00 2001 From: Mathias-Boulay Date: Mon, 13 Jan 2025 01:09:54 +0100 Subject: [PATCH 34/34] tweak(ui): make the keyboard shift the input For some reason, when entering the settings fragment, it would have this behavior. --- app_pojavlauncher/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app_pojavlauncher/src/main/AndroidManifest.xml b/app_pojavlauncher/src/main/AndroidManifest.xml index 740e44964b..ebaf69481c 100644 --- a/app_pojavlauncher/src/main/AndroidManifest.xml +++ b/app_pojavlauncher/src/main/AndroidManifest.xml @@ -55,7 +55,8 @@ + android:label="@string/app_short_name" + android:windowSoftInputMode="adjustResize"/>