Androd S/T 系统应用不支持 DragAndDrop 特性问题分析

625 阅读4分钟

关键字

Android 12、13;View.startDragAndDrop;系统应用;系统BUG;

一、问题描述

1.1 问题现象

在Android S/T 平台,系统应用(带 android:sharedUserId="android.uid.system" 属性)无法使用 View.startDragAndDrop 特性,调用该方法时报错:

Unable to initiate drag
java.lang.IllegalStateException: Need to validate before calling identify is cleared 
    at android.os.Parcel.createExceptionOrNull(Parcel.java:3019)  
    at android.os.Parcel.createException(Parcel.java:2995)  
    at android.os.Parcel.readException(Parcel.java:2978)  
    at android.os.Parcel.readException(Parcel.java:2920)  
    at android.view.IWindowSession$Stub$Proxy.performDrag(IWindowSession.java:1635)  
    at android.view.View.startDragAndDrop(View.java:27343)

1.2 复现步骤

// 长按启用 DragAndDrop
btn.setOnLongClickListener { v ->
    v.startDragAndDrop(null, DragShadowBuilder(v), v, 0)
    true
}
 
// 监听Drag事件
btn.setOnDragListener { _, event ->
    when (event.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // 拖拽操作开始时触发的事件
        }
        DragEvent.ACTION_DRAG_ENTERED -> {
            // 当拖拽阴影(拖拽的视图的虚影)进入视图的边界时触发的事件
        }
        DragEvent.ACTION_DRAG_LOCATION -> {
            // 当拖拽阴影在接受拖拽事件的视图内移动时触发的事件
        }
        DragEvent.ACTION_DRAG_EXITED -> {
            // 当拖拽阴影离开视图边界时触发的事件
        }
        DragEvent.ACTION_DRAG_ENDED -> {
            // 拖拽操作完全结束时触发的事件
        }
    }
    true
}

1.3 影响范围

在 Android 12 与 13 版本,系统应用无法使用 DragAndDrop 特性。

二、问题分析

调用View.startDragAndDrop时,完整异常堆栈如下:

Unable to initiate drag  
java.lang.IllegalStateException: Need to validate before calling identify is cleared  
    at android.os.Parcel.createExceptionOrNull(Parcel.java:3019)  
    at android.os.Parcel.createException(Parcel.java:2995)  
    at android.os.Parcel.readException(Parcel.java:2978)  
    at android.os.Parcel.readException(Parcel.java:2920)  
    at android.view.IWindowSession$Stub$Proxy.performDrag(IWindowSession.java:1635)  
    at android.view.View.startDragAndDrop(View.java:27343)  
    ...  
Caused by: android.os.RemoteException: Remote stack trace:  
at com.android.server.wm.Session.validateAndResolveDragMimeTypeExtras(Session.java:371) 
at com.android.server.wm.Session.performDrag(Session.java:326) 
at android.view.IWindowSession$Stub.onTransact(IWindowSession.java:856)  
at com.android.server.wm.Session.onTransact(Session.java:178)  
at android.os.Binder.execTransactInternal(Binder.java:1280)

从以上堆栈可以看出,报错入口在 Session.validateAndResolveDragMimeTypeExtras 方法。

2.1 源码分析

打开 API 32 和 33,发现在Session.performDrag 方法中会同时调用 validateAndResolveDragMimeTypeExtrasvalidateDragFlags,方法内判断 callingUid 如果是 Process.SYSTEM_UID 就抛出该异常:Need to validate before calling identify is cleared

@Override
public IBinder performDrag(IWindow window, int flags, SurfaceControl surface, int touchSource,
        float touchX, float touchY, float thumbCenterX, float thumbCenterY, ClipData data) {
    final int callingUid = Binder.getCallingUid();
    final int callingPid = Binder.getCallingPid();
    // Validate and resolve ClipDescription data before clearing the calling identity
    validateAndResolveDragMimeTypeExtras(data, callingUid, callingPid, mPackageName);
    validateDragFlags(flags, callingUid);
    final long ident = Binder.clearCallingIdentity();
    try {
        return mDragDropController.performDrag(mPid, mUid, window, flags, surface, touchSource,
                touchX, touchY, thumbCenterX, thumbCenterY, data);
    } finally {
        Binder.restoreCallingIdentity(ident);
    }
}


/**
 * Validates the given drag flags.
 */
@VisibleForTesting
void validateDragFlags(int flags, int callingUid) {
    if (callingUid == Process.SYSTEM_UID) {
        throw new IllegalStateException("Need to validate before calling identify is cleared");
    }
    if ((flags & View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION) != 0) {
        if (!mCanStartTasksFromRecents) {
            throw new SecurityException("Requires START_TASKS_FROM_RECENTS permission");
        }
    }
}



@VisibleForTesting
void validateAndResolveDragMimeTypeExtras(ClipData data, int callingUid, int callingPid,
        String callingPackage) {
    if (callingUid == Process.SYSTEM_UID) {
        throw new IllegalStateException("Need to validate before calling identify is cleared");
    }
    final ClipDescription desc = data != null ? data.getDescription() : null;
    if (desc == null) {
        return;
    }
    ...
}

而 API 30 中 performDrag 方法则没有 validateAndResolveDragMimeTypeExtrasvalidateDragFlags 这两个方法:

@Override  
public IBinder performDrag(IWindow window, int flags, SurfaceControl surface, int touchSource,  
        float touchX, float touchY, float thumbCenterX, float thumbCenterY, ClipData data) {  
    final long ident = Binder.clearCallingIdentity();  
    try {  
        return mDragDropController.performDrag(mSurfaceSession, mPid, mUid, window,  
                flags, surface, touchSource, touchX, touchY, thumbCenterX, thumbCenterY, data);  
    } finally {  
        Binder.restoreCallingIdentity(ident);  
    }  
}

同时查看 API 34,发现 validateAndResolveDragMimeTypeExtras 和 validateDragFlags 已经移除了对 callingUid 的判断:

@Override
public IBinder performDrag(IWindow window, int flags, SurfaceControl surface, int touchSource,
        float touchX, float touchY, float thumbCenterX, float thumbCenterY, ClipData data) {
    // Validate and resolve ClipDescription data before clearing the calling identity
    validateAndResolveDragMimeTypeExtras(data, Binder.getCallingUid(), Binder.getCallingPid(),
            mPackageName);
    validateDragFlags(flags, callingUid);
    final long ident = Binder.clearCallingIdentity();
    try {
        return mDragDropController.performDrag(mPid, mUid, window, flags, surface, touchSource,
                touchX, touchY, thumbCenterX, thumbCenterY, data);
    } finally {
        Binder.restoreCallingIdentity(ident);
    }
}

/**
 * Validates the given drag flags.
 */
@VisibleForTesting
void validateDragFlags(int flags, int callingUid) {
    if ((flags & View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION) != 0) {
        if (!mCanStartTasksFromRecents) {
            throw new SecurityException("Requires START_TASKS_FROM_RECENTS permission");
        }
    }
}

@VisibleForTesting
void validateAndResolveDragMimeTypeExtras(ClipData data, int callingUid, int callingPid,
        String callingPackage) {
    final ClipDescription desc = data != null ? data.getDescription() : null;
    if (desc == null) {
        return;
    }
    ...
}

可以看出,该问题是由原生改动导致的。

自 Android12 加上了 callingUid 的判断,限制系统应用使用 View.startDragAndDrop ,而 Android 14 又移除了该限制,由此推断该限制是错误引入的。

2.2 问题追溯

此问题最早由小米反馈给Google,并给出了解决方案:

24090de9dcd11cc61e9bb832214e5366.png

2022年9月8日,这笔提交被Google合并。

a3f9e925b085dd5099f8cec0d1b0ab25.png

三、解决方案

在Android S 和 Android T版本,由系统参考 Android U版本,移除 validateAndResolveDragMimeTypeExtras 与 validateDragFlags 方法中对 callingUid 的判断。

源文件位置:frameworks/base/services/core/java/com/android/server/wm/Session.java

xrefandroid.com/android-14.…

@Override
public IBinder performDrag(IWindow window, int flags, SurfaceControl surface, int touchSource,
        float touchX, float touchY, float thumbCenterX, float thumbCenterY, ClipData data) {
    // Validate and resolve ClipDescription data before clearing the calling identity
    validateAndResolveDragMimeTypeExtras(data, Binder.getCallingUid(), Binder.getCallingPid(),
            mPackageName);
    final long ident = Binder.clearCallingIdentity();
    try {
        return mDragDropController.performDrag(mPid, mUid, window, flags, surface, touchSource,
                touchX, touchY, thumbCenterX, thumbCenterY, data);
    } finally {
        Binder.restoreCallingIdentity(ident);
    }
}

@VisibleForTesting
void validateDragFlags(int flags, int callingUid) {
    // 移除这里判断
    if (callingUid == Process.SYSTEM_UID) {
        throw new IllegalStateException("Need to validate before calling identify is cleared");
    }
    if ((flags & View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION) != 0) {
        if (!mCanStartTasksFromRecents) {
            throw new SecurityException("Requires START_TASKS_FROM_RECENTS permission");
        }
    }
}

@VisibleForTesting
void validateAndResolveDragMimeTypeExtras(ClipData data, int callingUid, int callingPid,
        String callingPackage) {

    // 移除这里判断
    if (callingUid == Process.SYSTEM_UID) {
        throw new IllegalStateException("Need to validate before calling identify is cleared");
    }

    final ClipDescription desc = data != null ? data.getDescription() : null;
    if (desc == null) {
        return;
    }
    ...
}

四、问题总结

这个问题是原生大版本改动导致的,常规代码review流程无法覆盖到。

普通应用使用 View.startDragAndDrop 无法发现此问题,需要应用带上 SYSTEM_UID 才能复现。

这一类和应用无关、跨 Android 版本出现的问题,需要从源头进行排查,对关键方法前后修改点逐一分析,才能确定原因并找出针对措施。

系统 API 并不是完全可靠的,遇到系统 BUG 可以优先做好兼容并及时向 Google 反馈建议。

五、参考文档