- 作者:老汪软件技巧
- 发表时间:2024-12-28 15:05
- 浏览量:
前言
先声明一下,我不是米黑,虽然近一年它解BL锁/ROOT的机会越来越封闭了,但至少我接触搞机之后自己ROOT了的设备只有红米,降低过我认识Android的成本,我未来还会继续支持。
不是别的某些手机没BUG,而是不能说,不敢说,甚至有的竟然还把framework.jar混淆了(是为了干扰别人分析、研究、学习、定位bug的混淆,而不是为了减小体积的压缩,这种我都是劝人别买)。
上一次分析BUG归属:短信出现神秘编码,是微信还是MIUI的锅?
我也并没有如题标题所说在这里“背锅”,因为这个api只是出于兴趣尝试一下,并没有真的需要去用,但是这种证明问题出自于系统而不是自己代码的过程,在各种手机上确实经历过几次,也都是这个探究思路。
本文仅供参考,欢迎点评纠错。
问题现场
对于全面屏手机,屏幕边缘的View很难拖拽,明明应该按到了,却就是拖不动;如果不小心超过某个角度,甚至就触发返回手势导致当前页面finish。
"1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<View
android:id="@+id/view"
android:layout_width="60dp"
android:layout_height="60dp"
android:background="#9575CD"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
androidx.constraintlayout.widget.ConstraintLayout>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityTestBinding.inflate(layoutInflater)
setContentView(binding.root)
var pressX = 0f
var pressY = 0f
var top = 0
var left = 0
val lp = binding.view.layoutParams as ConstraintLayout.LayoutParams
@Suppress("ClickableViewAccessibility")
binding.view.setOnTouchListener { v, event ->
if(event.actionMasked == MotionEvent.ACTION_DOWN){
pressX = event.rawX
pressY = event.rawY
top = lp.topMargin
left = lp.leftMargin
}else if(event.actionMasked == MotionEvent.ACTION_MOVE){
pressX - event.rawX
pressY - event.rawY
lp.topMargin = (top + (event.rawY - pressY)).toInt()
lp.leftMargin = (left + (event.rawX - pressX)).toInt()
binding.view.layoutParams = lp
}
true
}
}
难以拖拽,显然这是app手势和全面屏手势有冲突导致的问题。
查找资料很轻易就能发现View类有一个setSystemGestureExclusionRects方法,专用于设置某个区域范围不接受全面屏相关手势。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
binding.view.addOnLayoutChangeListener { v, left, top, right, bottom, _, _, _, _ ->
v.systemGestureExclusionRects = listOf(
Rect(0, 0, right - left, bottom - top))
}
}
但我发现并没有效果??
我最开始是误以为是自己滑动太快,手指按下时,根本没有按到屏幕的滑块。于是开启“显示触摸位置”(开发者选项中的“小白点”,以及show taps(基于adb shell getevent),多次录屏进行确认。
但我发现不能信任这个小白点,因为我才发现了一个特性:手指在最开始接触屏幕两侧边缘的一瞬间,App不会收到ACTION_DOWN,甚至adb shell getevent第一时间内也看不到对应的“按下”事件,而是要先等系统通过接下来的动作决定是否触发手势。
所以实际录屏看到的(每个时刻的)小白点位置和我手指实际位置并不一样。
调整各种使用姿势仍然无效,以我一年多的各种机型适配经验,直接怀疑问题不在于我。用miui setSystemGestureExclusionRects作为关键词却未找到任何博客,随手在几个技术群里问了问,收到唯一的回复就是"miui没实现这个api"。
API被删了吗?导出framework.jar确认
真的是这个功能直接被删了吗?android.view.View属于Android的framework部分,如果是被删了,我首先会怀疑是直接清空了这个方法内部代码,那么只需要导出/system/framework/framwork.jar拖到jadx看一眼就知道了。
然而这个方法实际是完整的,和aosp逻辑看起来没什么区别。接下来顺着这个函数找到最终的AIDL接口。因为触摸事件到达Window之前就要确定分发到哪里,仅仅是App本地存一个变量是没意义的。
为了调试起来比较快,我们直接用Pine去hook这个AIDL接口,确定一下是否是中间某一步被阉割或者存在BUG。
Pine.disableHiddenApiPolicy(true, true)
@Suppress("PrivateApi")
val clazz = Class.forName("android.view.IWindowSession$Stub$Proxy")
for (field in clazz.declaredMethods) {
if(field.name == "reportSystemGestureExclusionChanged"){
Pine.hook(field, object: MethodHook() {
override fun beforeCall(callFrame: Pine.CallFrame) {
super.beforeCall(callFrame)
Log.e(TAG, "beforeCall: ${Arrays.toString(callFrame.args)}", Throwable());
}
})
}
}
结果是,AIDL接口调用成功,参数也正常,至少已经证明这个功能不是在App端被阉割的。接下来可以向Service端找找线索。reportSystemGestureExclusionChanged的具体实现并不在framework.jar,而是services.jar,这里本文不继续分析了。一个判断类在哪个jar的技巧:
sunstone:/ $ cd system/framework/
sunstone:/system/framework $ grep -r 'reportSystemGestureExclusionChanged'
Binary file ./framework.jar matches
Binary file ./framework.jar matches
Binary file ./services.jar matches
难道我调用错了?再从SDK代码里找线索
既然从API看来一切正常,手边又没其他手机,接下来再怀疑一下是不是自己调用问题。我习惯是直接先用AndroidStudio搜索关键词,看看系统或第三方库是怎么用的API。
发现到PointerLocationView类有相关的API。这个类很有意思,普通App也可以通过反射或者创建Stub类去添加它。这就是开发者选项中的“指针位置”。
当勾选它之后,system_server进程会创建一个PointerLocationView实例,通过WindowManager添加到屏幕(也就是我们常说的“悬浮窗”)。
开发者选项的“指针位置”还有个隐藏功能!
具体去看PointerLocationView,其实他有如下代码:
/**
* If set to a positive value between 1-255, shows an overlay with the approved (red) and
* rejected (blue) exclusions.
*/
private static final String GESTURE_EXCLUSION_PROP = "debug.pointerlocation.showexclusion";
@Override
protected void onAttachedToWindow() {
//...
if (shouldShowSystemGestureExclusion()) {
try {
WindowManagerGlobal.getWindowManagerService()
.registerSystemGestureExclusionListener(mSystemGestureExclusionListener,
mContext.getDisplayId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
final int alpha = systemGestureExclusionOpacity();
mSystemGestureExclusionPaint.setAlpha(alpha);
mSystemGestureExclusionRejectedPaint.setAlpha(alpha);
} else {
mSystemGestureExclusion.setEmpty();
}
//...
}
//...
private static boolean shouldShowSystemGestureExclusion() {
return systemGestureExclusionOpacity() > 0;
}
private static int systemGestureExclusionOpacity() {
int x = SystemProperties.getInt(GESTURE_EXCLUSION_PROP, 0);
return x >= 0 && x <= 255 ? x : 0;
}
这个功能就是把系统已排除了的手势区域标记为红色,但他要通过debug.pointerlocation.showexclusion获取透明度,从而判断是否开启这个功能。
通过adb设置一个透明度过去:setprop debug.pointerlocation.showexclusion 188,重新打开这个开关。可以看到我们刚刚设置了的手势的View的区域上方被绘制为了红色。
这又证明api调用方面是没问题的。难道不得不去分析services.jar了吗?并不是,仔细看下面这张图,当我们的View移动到屏幕的右侧、底部、左侧的时候,发现我们的排除手势区域(红色区域)少了一部分,似乎还刚好是全面屏手势触发区域的一部分。
那么也就找到原因了,有神秘透明Window盖在了我们app上方,导致我们设置的部分排除区域被系统忽略。
谁搞的Window
是谁在我屏幕上添加了透明window?我们可以通过dumpsys window | grep 'Window #' -A 10查看当前有哪些window。
忽略无关内容之后可以看到叫做GestureStubLeft、GestureStubLeft的东西,包名是miui的launcher包名com.miui.home:
Window #2 Window{cc0aa0d u0 PointerLocation - display 0}:
// 无关内容忽略
Window #4 Window{5e933ff u0 GestureStubRight}:
mDisplayId=0 rootTaskId=1 mSession=Session{1a92528 2776:u0a10108} mClient=android.os.BinderProxy@db61759
mOwnerUid=10108 showForAllUsers=true package=com.miui.home appop=NONE
mAttrs={(0,0)(54x1440) vm=0.028645834 gr=BOTTOM RIGHT CENTER sim={adjust=pan} layoutInDisplayCutoutMode=shortEdges disableCutout=false ty=MAGNIFICATION_OVERLAY fmt=RGBA_8888
fl=NOT_FOCUSABLE NOT_TOUCH_MODAL LAYOUT_IN_SCREEN
pfl=USE_BLAST
bhv=DEFAULT
fitTypes=NAVIGATION_BARS CAPTION_BAR}
Requested w=54 h=1440 mLayoutSeq=291068
mHasSurface=true isReadyForDisplay()=true canReceiveKeys()=false mWindowRemovalAllowed=false
WindowStateAnimator{aeb3a00 GestureStubRight}:
--
Window #5 Window{e0eb4b3 u0 GestureStubLeft}:
mDisplayId=0 rootTaskId=1 mSession=Session{1a92528 2776:u0a10108} mClient=android.os.BinderProxy@6202db1
mOwnerUid=10108 showForAllUsers=true package=com.miui.home appop=NONE
mAttrs={(0,0)(54x1440) vm=0.028645834 gr=BOTTOM LEFT CENTER sim={adjust=pan} layoutInDisplayCutoutMode=shortEdges disableCutout=false ty=MAGNIFICATION_OVERLAY fmt=RGBA_8888
fl=NOT_FOCUSABLE NOT_TOUCH_MODAL LAYOUT_IN_SCREEN
pfl=USE_BLAST
bhv=DEFAULT
fitTypes=NAVIGATION_BARS CAPTION_BAR}
Requested w=54 h=1440 mLayoutSeq=291068
mHasSurface=true isReadyForDisplay()=true canReceiveKeys()=false mWindowRemovalAllowed=false
WindowStateAnimator{33ae07e GestureStubLeft}:
// 别的什么 控制中心 状态条 之类的这里全都忽略
Window #12 Window{a3cd8a5 u0 com.miui.screenrecorder}:
Window #19 Window{33017e3 u0 这里是我们测试app}:
也就是说这个透明窗口是launcher添加的,通过killall com.miui.home,在launcher被自动重启前的1~2秒可以看到我们的"红色区域"没有被裁剪,并且可以成功贴着右侧屏幕边缘,水平向左拖拽我们的View。这也说明了MIUI的全面屏手势是依赖launcher的“透明悬浮窗”的,但这个悬浮窗也导致它下方的与它重叠的“全面屏排除区域”失去效果。
也可以从它launcher代码以及logcat看到是通过悬浮窗口获取到侧滑的触摸事件,然后通过InputManager的injectInputEvent模拟返回键。
分析至此也就证明了这个问题对于普通app是无解的,总不能再自己创建个更高层级的悬浮窗吧...
总结
MIUI在2018年就开始支持全面屏手势,而Google在2019年发布的Android10才开始实现。原本小米遥遥领先的功能,却由于历史原因导致后来的View#setSystemGestureExclusionRects失效。
“代码是负债,不是资产”!是时候排期改掉全面屏的实现了。或许某个版本已经解决了?我没有最新的小米手机去验证。