Skip to content

Commit 7612df8

Browse files
add multiple snapping mods
1 parent 38d9ee0 commit 7612df8

File tree

18 files changed

+580
-44
lines changed

18 files changed

+580
-44
lines changed

SplitScreenMods/Readme.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ Keep the split screen ratio, when switching one of the split apps.
1717
Only usable on Android 14 and later,
1818
since previous versions did not force a split resize in this situation.
1919

20+
## SnapMode
21+
22+
Change the snap mode used by the system:
23+
24+
* `16:9` snapping useful for playing videos in split screen
25+
* `FIXED` snapping allows for custom ratios and [additional snap targets](#additionalsnaptargets)
26+
* `1:1` only allows the middle 50:50 split
27+
2028
## FreeSnap
2129

2230
Allow any split ratio instead of snapping to predefined ratios.
31+
32+
## AdditionalSnapTargets
33+
34+
Add additional snapping targets to snap to.
35+
36+
## RemoveMinimalTaskSize
37+
38+
Remove the minimal task size limit to allow split screen with unreasonably small apps.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
com.programminghoch10.SplitScreenMods.AdditionalSnapTargetsHook
12
com.programminghoch10.SplitScreenMods.AlwaysAllowMultiInstanceSplitHook
3+
com.programminghoch10.SplitScreenMods.CalculateRatiosHook
4+
com.programminghoch10.SplitScreenMods.CustomFixedRatioHook
25
com.programminghoch10.SplitScreenMods.FreeSnapHook
36
com.programminghoch10.SplitScreenMods.KeepSplitScreenRatioHook
7+
com.programminghoch10.SplitScreenMods.RemoveMinimalTaskSizeHook
8+
com.programminghoch10.SplitScreenMods.SnapModeHook
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.programminghoch10.SplitScreenMods
2+
3+
import android.content.res.Resources
4+
import android.graphics.Rect
5+
import android.os.Build
6+
import com.programminghoch10.SplitScreenMods.AdditionalSnapTargetsHookConfig.enabled
7+
import com.programminghoch10.SplitScreenMods.BuildConfig.SHARED_PREFERENCES_NAME
8+
import de.binarynoise.logger.Logger.log
9+
import de.robv.android.xposed.IXposedHookInitPackageResources
10+
import de.robv.android.xposed.IXposedHookLoadPackage
11+
import de.robv.android.xposed.XC_MethodHook
12+
import de.robv.android.xposed.XSharedPreferences
13+
import de.robv.android.xposed.XposedBridge
14+
import de.robv.android.xposed.XposedHelpers
15+
import de.robv.android.xposed.callbacks.XC_InitPackageResources
16+
import de.robv.android.xposed.callbacks.XC_LoadPackage
17+
18+
object AdditionalSnapTargetsHookConfig {
19+
val enabled = SnapModeHookConfig.enabled && CustomFixedRatioHookConfig.enabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA
20+
}
21+
22+
class AdditionalSnapTargetsHook : IXposedHookLoadPackage, IXposedHookInitPackageResources {
23+
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
24+
if (lpparam.packageName != "com.android.systemui") return
25+
if (!enabled) return
26+
log("handleLoadPackage(${lpparam.packageName} in process ${lpparam.processName})")
27+
val preferences = XSharedPreferences(BuildConfig.APPLICATION_ID, SHARED_PREFERENCES_NAME)
28+
val selectedSnapTargetsString = preferences.getString("SnapTargets", "SYSTEM")!!
29+
if (selectedSnapTargetsString == "SYSTEM" || selectedSnapTargetsString == "CUSTOM") return
30+
val selectedSnapTargets = selectedSnapTargetsString.split(",").map { it.toFloat() }.sorted().drop(1)
31+
log("additional snap targets are ${selectedSnapTargets.joinToString()}")
32+
33+
if (selectedSnapTargets.isEmpty()) return // handled by resource hook below
34+
35+
val DividerSnapAlgorithmClass = XposedHelpers.findClass("com.android.wm.shell.common.split.DividerSnapAlgorithm", lpparam.classLoader)
36+
val SnapTargetClass = XposedHelpers.findClass(DividerSnapAlgorithmClass.name + "\$SnapTarget", lpparam.classLoader)
37+
val SnapTargetClassConstructor = XposedHelpers.findConstructorExact(SnapTargetClass, Int::class.java, Int::class.java)
38+
39+
XposedBridge.hookAllConstructors(DividerSnapAlgorithmClass, object : XC_MethodHook() {
40+
override fun afterHookedMethod(param: MethodHookParam) {
41+
log("after ${DividerSnapAlgorithmClass.simpleName} constructor")
42+
val res = param.args[0] as Resources
43+
val mTargets = XposedHelpers.getObjectField(param.thisObject, "mTargets") as ArrayList<Any?>
44+
if (mTargets.isEmpty()) return
45+
46+
// reimplementation of inlined methods getStartInset() and getEndInset()
47+
val mIsLeftRightSplit = XposedHelpers.getBooleanField(param.thisObject, "mIsLeftRightSplit")
48+
val mInsets = XposedHelpers.getObjectField(param.thisObject, "mInsets") as Rect
49+
val startInset = if (mIsLeftRightSplit) mInsets.left else mInsets.top
50+
val endInset = if (mIsLeftRightSplit) mInsets.right else mInsets.bottom
51+
52+
// basically reimplemented addFixedDivisionTargets
53+
val mDisplayWidth = XposedHelpers.getIntField(param.thisObject, "mDisplayWidth")
54+
val mDisplayHeight = XposedHelpers.getIntField(param.thisObject, "mDisplayHeight")
55+
val start = startInset
56+
val end = if (mIsLeftRightSplit) mDisplayWidth - endInset else mDisplayHeight - endInset
57+
val mDividerSize = XposedHelpers.getIntField(param.thisObject, "mDividerSize")
58+
val mCalculateRatiosBasedOnAvailableSpace = getCalculateRatiosBasedOnAvailableSpace(res)
59+
log("mCalculateRatiosBasedOnAvailableSpace=$mCalculateRatiosBasedOnAvailableSpace")
60+
val mMinimalSizeResizableTask = XposedHelpers.getIntField(param.thisObject, "mMinimalSizeResizableTask")
61+
log("mMinimalSizeResizableTask=$mMinimalSizeResizableTask")
62+
fun getSize(ratio: Float): Int {
63+
var size = (ratio * (end - start)).toInt() - mDividerSize / 2
64+
if (mCalculateRatiosBasedOnAvailableSpace) size = Math.max(size, mMinimalSizeResizableTask)
65+
return size
66+
}
67+
68+
for (snapTargetRatio in selectedSnapTargets) {
69+
val size = getSize(snapTargetRatio)
70+
val startPosition = start + size
71+
val endPosition = end - size
72+
73+
// slightly changed reimplementation of inlined method maybeAddTarget()
74+
fun maybeAddTarget(position: Int, size: Int, snapPosition: Int?) {
75+
if (size < mMinimalSizeResizableTask) {
76+
log("Cannot add snap target ${snapTargetRatio} because it's size of ${size} would be less than minimal size ${mMinimalSizeResizableTask}!")
77+
return
78+
}
79+
log("adding snap target ${snapTargetRatio} at $position of size $size")
80+
val snapTarget = SnapTargetClassConstructor.newInstance(position, snapPosition ?: SNAP_TO_NONE)
81+
mTargets.add(snapTarget)
82+
}
83+
84+
maybeAddTarget(startPosition, startPosition - startInset, SNAP_TO_2_33_66)
85+
maybeAddTarget(endPosition, end - endPosition, SNAP_TO_2_66_33)
86+
}
87+
88+
log("final mTargets[${mTargets.size}]: ${mTargets.joinToString()}")
89+
}
90+
})
91+
}
92+
93+
fun getCalculateRatiosBasedOnAvailableSpace(res: Resources): Boolean {
94+
val mCalculateRatiosBasedOnAvailableSpaceId = res.getIdentifier("config_flexibleSplitRatios", "bool", "android")
95+
return res.getBoolean(mCalculateRatiosBasedOnAvailableSpaceId)
96+
}
97+
98+
override fun handleInitPackageResources(resparam: XC_InitPackageResources.InitPackageResourcesParam) {
99+
if (resparam.packageName != "com.android.systemui") return
100+
if (!enabled) return
101+
log("handleInitPackageResources(${resparam.packageName})")
102+
val preferences = XSharedPreferences(BuildConfig.APPLICATION_ID, SHARED_PREFERENCES_NAME)
103+
val selectedSnapTargetsString = preferences.getString("SnapTargets", "SYSTEM")!!
104+
if (selectedSnapTargetsString == "SYSTEM" || selectedSnapTargetsString == "CUSTOM") return
105+
val selectedSnapTargets = selectedSnapTargetsString.split(",")
106+
if (selectedSnapTargets.isEmpty()) return
107+
val customRatio = selectedSnapTargets.map { it.toFloat() }.minOf { it }
108+
log("min split ratio is ${customRatio}")
109+
if (customRatio < 0 || customRatio >= 0.5f) return
110+
CustomFixedRatioHook.overrideFixedRatio(resparam, customRatio)
111+
}
112+
}

SplitScreenMods/src/main/java/com/programminghoch10/SplitScreenMods/AlwaysAllowMultiInstanceSplitHook.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,8 @@ class AlwaysAllowMultiInstanceSplitHook : IXposedHookLoadPackage {
6363
}
6464
"android" -> {
6565
try {
66-
val ActivityStarterClass =
67-
Class.forName("com.android.server.wm.ActivityStarter", false, lpparam.classLoader)
68-
val ActivityStarterRequestClass =
69-
Class.forName(ActivityStarterClass.name + "\$Request", false, lpparam.classLoader)
66+
val ActivityStarterClass = Class.forName("com.android.server.wm.ActivityStarter", false, lpparam.classLoader)
67+
val ActivityStarterRequestClass = Class.forName(ActivityStarterClass.name + "\$Request", false, lpparam.classLoader)
7068
XposedHelpers.findAndHookMethod(
7169
ActivityStarterClass,
7270
"executeRequest",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.programminghoch10.SplitScreenMods
2+
3+
import com.programminghoch10.SplitScreenMods.BuildConfig.SHARED_PREFERENCES_NAME
4+
import com.programminghoch10.SplitScreenMods.CalculateRatiosHookConfig.enabled
5+
import de.binarynoise.logger.Logger.log
6+
import de.robv.android.xposed.IXposedHookInitPackageResources
7+
import de.robv.android.xposed.XSharedPreferences
8+
import de.robv.android.xposed.callbacks.XC_InitPackageResources
9+
10+
object CalculateRatiosHookConfig {
11+
val enabled = SnapModeHookConfig.enabled
12+
}
13+
14+
class CalculateRatiosHook : IXposedHookInitPackageResources {
15+
override fun handleInitPackageResources(resparam: XC_InitPackageResources.InitPackageResourcesParam) {
16+
if (resparam.packageName != "com.android.systemui") return
17+
if (!enabled) return
18+
log("handleInitPackageResources(${resparam.packageName})")
19+
val preferences = XSharedPreferences(BuildConfig.APPLICATION_ID, SHARED_PREFERENCES_NAME)
20+
if (!preferences.contains("CalculateRatios")) return
21+
22+
val enabled = preferences.getBoolean("CalculateRatios", false)
23+
log("set config_flexibleSplitRatios to $enabled")
24+
resparam.res.setReplacement("android", "bool", "config_flexibleSplitRatios", enabled)
25+
}
26+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.programminghoch10.SplitScreenMods
2+
3+
import android.content.res.AssetManager
4+
import android.content.res.Configuration
5+
import android.content.res.Resources
6+
import android.content.res.XResForwarder
7+
import android.util.DisplayMetrics
8+
import com.programminghoch10.SplitScreenMods.BuildConfig.SHARED_PREFERENCES_NAME
9+
import com.programminghoch10.SplitScreenMods.CustomFixedRatioHookConfig.enabled
10+
import de.binarynoise.logger.Logger.log
11+
import de.robv.android.xposed.IXposedHookInitPackageResources
12+
import de.robv.android.xposed.XSharedPreferences
13+
import de.robv.android.xposed.XposedHelpers
14+
import de.robv.android.xposed.callbacks.XC_InitPackageResources
15+
16+
object CustomFixedRatioHookConfig {
17+
val enabled = SnapModeHookConfig.enabled
18+
}
19+
20+
class CustomFixedRatioHook : IXposedHookInitPackageResources {
21+
override fun handleInitPackageResources(resparam: XC_InitPackageResources.InitPackageResourcesParam) {
22+
if (resparam.packageName != "com.android.systemui") return
23+
if (!enabled) return
24+
log("handleInitPackageResources(${resparam.packageName})")
25+
val preferences = XSharedPreferences(BuildConfig.APPLICATION_ID, SHARED_PREFERENCES_NAME)
26+
if (preferences.getString("SnapMode", "SYSTEM") != "FIXED") return
27+
val selectedSnapTargetsString = preferences.getString("SnapTargets", "SYSTEM")!!
28+
if (selectedSnapTargetsString != "CUSTOM") return
29+
if (!preferences.contains("CustomRatio")) return
30+
val customFixedRatio = preferences.getInt("CustomRatio", 33) / 100f
31+
overrideFixedRatio(resparam, customFixedRatio)
32+
}
33+
34+
companion object {
35+
fun overrideFixedRatio(resparam: XC_InitPackageResources.InitPackageResourcesParam, customFixedRatio: Float) {
36+
log("overriding fixed ratio with $customFixedRatio")
37+
resparam.res.setReplacement("android", "fraction", "docked_stack_divider_fixed_ratio", FractionReplacement(customFixedRatio))
38+
}
39+
40+
fun FractionReplacement(fraction: Float): XResForwarder {
41+
val assetManager = XposedHelpers.getStaticObjectField(AssetManager::class.java, "sSystem") as AssetManager
42+
val res = object : Resources(assetManager, DisplayMetrics(), Configuration()) {
43+
override fun getFraction(id: Int, base: Int, pbase: Int): Float {
44+
return fraction
45+
}
46+
}
47+
return XResForwarder(res, 0)
48+
}
49+
}
50+
}

SplitScreenMods/src/main/java/com/programminghoch10/SplitScreenMods/FreeSnapHook.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ class FreeSnapHook : IXposedHookLoadPackage {
2525
val enabled = preferences.getBoolean("FreeSnap", false)
2626
if (!enabled) return
2727

28-
val DividerSnapAlgorithmClass =
29-
XposedHelpers.findClass("com.android.wm.shell.common.split.DividerSnapAlgorithm", lpparam.classLoader)
28+
val DividerSnapAlgorithmClass = XposedHelpers.findClass("com.android.wm.shell.common.split.DividerSnapAlgorithm", lpparam.classLoader)
3029
XposedBridge.hookAllConstructors(DividerSnapAlgorithmClass, object : XC_MethodHook() {
3130
override fun afterHookedMethod(param: MethodHookParam) {
3231
XposedHelpers.setBooleanField(param.thisObject, "mFreeSnapMode", true)

SplitScreenMods/src/main/java/com/programminghoch10/SplitScreenMods/KeepSplitScreenRatioHook.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,8 @@ class KeepSplitScreenRatioHook : IXposedHookLoadPackage {
2929
val enabled = preferences.getBoolean("KeepSplitScreenRatio", false)
3030
if (!enabled) return
3131

32-
val SplitLayoutClass =
33-
XposedHelpers.findClass("com.android.wm.shell.common.split.SplitLayout", lpparam.classLoader)
34-
val StageCoordinatorClass =
35-
XposedHelpers.findClass("com.android.wm.shell.splitscreen.StageCoordinator", lpparam.classLoader)
32+
val SplitLayoutClass = XposedHelpers.findClass("com.android.wm.shell.common.split.SplitLayout", lpparam.classLoader)
33+
val StageCoordinatorClass = XposedHelpers.findClass("com.android.wm.shell.splitscreen.StageCoordinator", lpparam.classLoader)
3634

3735
/*
3836
Currently this module disables SplitScreen enter and dismiss animations completely.
@@ -100,8 +98,7 @@ class KeepSplitScreenRatioHook : IXposedHookLoadPackage {
10098
val binder = param.args[0] as IBinder
10199
val stageCoordinator = param.thisObject
102100
val mSplitTransitions = XposedHelpers.getObjectField(stageCoordinator, "mSplitTransitions")
103-
val isPendingEnter =
104-
XposedHelpers.callMethod(mSplitTransitions, "isPendingEnter", binder) as Boolean
101+
val isPendingEnter = XposedHelpers.callMethod(mSplitTransitions, "isPendingEnter", binder) as Boolean
105102
log("${startPendingAnimationMethod.name}: isPendingEnter=$isPendingEnter")
106103
if (!isPendingEnter) return
107104
val mPendingEnter = XposedHelpers.getObjectField(mSplitTransitions, "mPendingEnter")
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.programminghoch10.SplitScreenMods
2+
3+
import android.content.res.XResources
4+
import android.os.Build
5+
import android.util.TypedValue
6+
import com.programminghoch10.SplitScreenMods.BuildConfig.SHARED_PREFERENCES_NAME
7+
import com.programminghoch10.SplitScreenMods.RemoveMinimalTaskSizeHookConfig.enabled
8+
import de.binarynoise.logger.Logger.log
9+
import de.robv.android.xposed.IXposedHookInitPackageResources
10+
import de.robv.android.xposed.XSharedPreferences
11+
import de.robv.android.xposed.callbacks.XC_InitPackageResources
12+
13+
object RemoveMinimalTaskSizeHookConfig {
14+
@JvmField
15+
val enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA
16+
}
17+
18+
class RemoveMinimalTaskSizeHook : IXposedHookInitPackageResources {
19+
override fun handleInitPackageResources(resparam: XC_InitPackageResources.InitPackageResourcesParam) {
20+
if (resparam.packageName != "com.android.systemui") return
21+
if (!enabled) return
22+
log("handleInitPackageResources(${resparam.packageName})")
23+
val preferences = XSharedPreferences(BuildConfig.APPLICATION_ID, SHARED_PREFERENCES_NAME)
24+
val enabled = preferences.getBoolean("RemoveMinimalTaskSize", false)
25+
if (!enabled) return
26+
27+
resparam.res.setReplacement(
28+
"android",
29+
"dimen",
30+
"default_minimal_size_resizable_task",
31+
object : XResources.DimensionReplacement(0f, TypedValue.COMPLEX_UNIT_DIP) {},
32+
)
33+
log("set replacement for default_minimal_size_resizable_task")
34+
}
35+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.programminghoch10.SplitScreenMods
2+
3+
// yoinked from https://github.com/LineageOS/android_frameworks_base/blob/2dc97cf3d6234c87497ca78b2734bb3ed604c349/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
4+
5+
enum class SNAP_MODE(val key: String, val value: Int) {
6+
7+
SNAP_MODE_UNCHANGED(
8+
"SYSTEM",
9+
-1,
10+
),
11+
12+
SNAP_MODE_16_9(
13+
"16_9",
14+
0,
15+
),
16+
17+
SNAP_FIXED_RATIO(
18+
"FIXED",
19+
1,
20+
),
21+
22+
SNAP_ONLY_1_1(
23+
"1_1",
24+
2,
25+
),
26+
27+
/*
28+
While this target does work,
29+
it is clearly intended for "select a second task to split" mode,
30+
where only one task's icon is shown as a preview for split selection.
31+
*/
32+
SNAP_MODE_MINIMIZED(
33+
"MINIMIZED",
34+
3,
35+
),
36+
37+
/*
38+
This target provides the offscreen ratios,
39+
but the required functionality seems to be stripped out on current builds.
40+
The fallback ratio is 33%, which can be obtained with the SNAP_FIXED_RATIO as well.
41+
*/
42+
SNAP_FLEXIBLE_SPLIT(
43+
"FLEXIBLE",
44+
4,
45+
),
46+
}

0 commit comments

Comments
 (0)