Android 系统副屏截屏功能
收到一个客户需求,要求对双屏设备的副屏进行截图。查询资料后发现,系统截图有两种方法,一种是通过SurfaceControl.screenshot提供的接口调用,还有一种是通过screencap 命令获取,这两种方式默认都需要使得系统签名才能使用。
-
方法一:SurfaceControl.screenshot
android 原生的音量减+电源键截屏功能最终会调用到在SysmteUI进程中。
frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible, int x, int y, int width, int height) {
......
// Take the screenshot
mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
if (mScreenBitmap == null) {
notifyScreenshotError(mContext, mNotificationManager,
R.string.screenshot_failed_to_capture_text);
finisher.run();
return;
}
......
从这个地方可以看到SurfaceControl.screenshot是执行方法,这里会返回当前桌面所在的截图,进入到screenshot方法
public static Bitmap screenshot(int width, int height) {
// TODO: should take the display as a parameter
IBinder displayToken = SurfaceControl.getBuiltInDisplay(
SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN);
return nativeScreenshot(displayToken, new Rect(), width, height, 0, 0, true,
false, Surface.ROTATION_0);
}
这里可以看到会调用一个native方法nativeScreenshot,第一个参数就是使用哪个屏幕,默认是BUILT_IN_DISPLAY_ID_MAIN主屏,后面几个参数是大小和方向,
由此可知道,当传递displayToken为副屏的display id时即可截取副屏。
基于此理论,我们在应用中写出相关代码测试
try {
@SuppressLint("PrivateApi") Class<?> mClassType = Class.forName("android.view.SurfaceControl");
Method nativeScreenshotMethod;
nativeScreenshotMethod = mClassType.getDeclaredMethod("nativeScreenshot", IBinder.class, Rect.class, int.class, int.class, int.class, int.class, boolean.class, boolean.class, int.class);
nativeScreenshotMethod.setAccessible(true);
Method getBuiltInDisplayMethod = mClassType.getMethod("getBuiltInDisplay", int.class);
IBinder displayToken = (IBinder)getBuiltInDisplayMethod.invoke(mClassType, 1);
// Log.d("MainActivity", "zly --> nativeScreenshotMethod before");
Bitmap sBitmap = (Bitmap)nativeScreenshotMethod.invoke(mClassType, displayToken, new Rect(), 1920, 1080, 0, 0, true, false, Surface.ROTATION_0);
// Log.d("MainActivity", "zly --> nativeScreenshotMethod after sBitmap=" + (sBitmap != null));
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
Log.d("MainActivity", "zly --> e:" + e.toString());
}
其中SurfaceControl是hide的类,所以必须要用反射,由于nativeScreenshot方法是private static 修饰的所以必须使用nativeScreenshotMethod.setAccessible(true);设置为可修改,否则会抛出异常。
在使用nativeScreenshot方法时,发现一直返回的NULL,无法获取返回的Bitmap,
网上查询资料后发现需求添加权限
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
但是添加完后,依然返回为空,于是把应用进行系统签名后可正常截图。这里可以确认到缺少对应权限导致,
于是查看log,发现一个权限报错的地方
Permission Denial:can't read framebuffer pid
找到对应代码位置
frameworks/native/services/surfaceflinger/SurfaceFlinger_hwc1.cpp
status_t SurfaceFlinger::onTransact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
switch (code) {
case CREATE_CONNECTION:
case CREATE_DISPLAY:
case SET_TRANSACTION_STATE:
case BOOT_FINISHED:
case CLEAR_ANIMATION_FRAME_STATS:
case GET_ANIMATION_FRAME_STATS:
case SET_POWER_MODE:
case GET_HDR_CAPABILITIES:
{
// codes that require permission check
IPCThreadState* ipc = IPCThreadState::self();
const int pid = ipc->getCallingPid();
const int uid = ipc->getCallingUid();
if ((uid != AID_GRAPHICS && uid != AID_SYSTEM) &&
!PermissionCache::checkPermission(sAccessSurfaceFlinger, pid, uid)) {
ALOGE("Permission Denial: "
"can't access SurfaceFlinger pid=%d, uid=%d", pid, uid);
return PERMISSION_DENIED;
}
break;
}
case CAPTURE_SCREEN:
{
// codes that require permission check
IPCThreadState* ipc = IPCThreadState::self();
const int pid = ipc->getCallingPid();
const int uid = ipc->getCallingUid();
if (false/* (uid != AID_GRAPHICS) &&
!PermissionCache::checkPermission(sReadFramebuffer, pid, uid)*/) {
ALOGE("Permission Denial: "
"can't read framebuffer pid=%d, uid=%d", pid, uid);
return PERMISSION_DENIED;
}
break;
}
}
uid != AID_GRAPHICS这个地方会导致第三方应用被拦截掉,屏蔽掉即可。
if (false/* (uid != AID_GRAPHICS) &&
!PermissionCache::checkPermission(sReadFramebuffer, pid, uid)*/) {
-
方法二:screencap命令方式
screencap命令参数如下
screencap [-hp] [-d display-id] [FILENAME]
-h: this message
-p: save the file as a png
-d: specify the display id to capture, default 0
T2:/ $ screencap -d 1 -p /sdcard/ff.png
使用这个命令即可截取副屏,-d后面对应的是屏幕id,0为主屏,1为副屏,
使用Runtime.getRuntime().exec命令即可
注:方法二没有进行尝试,应该需要系统签名才可以