• 作者:老汪软件技巧
  • 发表时间: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"。

image.png

API被删了吗?导出framework.jar确认

真的是这个功能直接被删了吗?android.view.View属于Android的framework部分,如果是被删了,我首先会怀疑是直接清空了这个方法内部代码,那么只需要导出/system/framework/framwork.jar拖到jadx看一眼就知道了。

image.png

然而这个方法实际是完整的,和aosp逻辑看起来没什么区别。接下来顺着这个函数找到最终的AIDL接口。因为触摸事件到达Window之前就要确定分发到哪里,仅仅是App本地存一个变量是没意义的。

image.png

为了调试起来比较快,我们直接用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());
            }
        })
    }
}

image.png

结果是,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

记一次手机厂商的BUG排查,App码农如何自证清白,不背锅:关于排除全面屏手势区域setSystemGestureExclusionRects不生效的问题_记一次手机厂商的BUG排查,App码农如何自证清白,不背锅:关于排除全面屏手势区域setSystemGestureExclusionRects不生效的问题_

Binary file ./services.jar matches

难道我调用错了?再从SDK代码里找线索

既然从API看来一切正常,手边又没其他手机,接下来再怀疑一下是不是自己调用问题。我习惯是直接先用AndroidStudio搜索关键词,看看系统或第三方库是怎么用的API。

image.png

发现到PointerLocationView类有相关的API。这个类很有意思,普通App也可以通过反射或者创建Stub类去添加它。这就是开发者选项中的“指针位置”。

image.png

当勾选它之后,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的区域上方被绘制为了红色。

image.png

这又证明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的“透明悬浮窗”的,但这个悬浮窗也导致它下方的与它重叠的“全面屏排除区域”失去效果。

image.png

也可以从它launcher代码以及logcat看到是通过悬浮窗口获取到侧滑的触摸事件,然后通过InputManager的injectInputEvent模拟返回键。

image.png

分析至此也就证明了这个问题对于普通app是无解的,总不能再自己创建个更高层级的悬浮窗吧...

总结

MIUI在2018年就开始支持全面屏手势,而Google在2019年发布的Android10才开始实现。原本小米遥遥领先的功能,却由于历史原因导致后来的View#setSystemGestureExclusionRects失效。

“代码是负债,不是资产”!是时候排期改掉全面屏的实现了。或许某个版本已经解决了?我没有最新的小米手机去验证。