关键字
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 方法中会同时调用 validateAndResolveDragMimeTypeExtras 和 validateDragFlags,方法内判断 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 方法则没有 validateAndResolveDragMimeTypeExtras 和 validateDragFlags 这两个方法:
@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,并给出了解决方案:
2022年9月8日,这笔提交被Google合并。
三、解决方案
在Android S 和 Android T版本,由系统参考 Android U版本,移除 validateAndResolveDragMimeTypeExtras 与 validateDragFlags 方法中对 callingUid 的判断。
源文件位置:frameworks/base/services/core/java/com/android/server/wm/Session.java
@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 反馈建议。