Android 截图时隐藏(穿透)特定的内容

前言

这是一种很神奇的操作,在截图时隐去特定的内容。这里的隐去并不是指像 FLAG_SECURE 这样无聊的禁止截图(在 Android S+ 表现为对应区域显示为黑色),而是彻底的假装内容不存在。想象一下:你的屏幕上有一个不透明的悬浮窗,但是一截图,那个窗口却和被关闭了一样,直接露出了后面的东西。

之前有听说过类似的需求,比如:打游戏开挂时开直播,不想让外挂界面被看到。
但终究这不是什么好东西,当时也完全没有兴趣。

但是最近突然又有正经的类似需求了,于是研究了一下相关机制,稍做记录。

以下内容基于 Android 12L。

Android 截图操作调用链

先来记录一下 电源 + 音量下 进行截图的调用过程大概是怎样的。

当 java 层第一次有机会拦截按键事件时(interceptKeyBeforeQueueing),组合键事件会被 KeyCombinationManager 拦截,并按照预先配置好的 TwoKeysCombinationRule ,调用 interceptScreenshotChord() ,从而开始截图之旅。

之后呢,会在对应的 Handler 上执行 ScreenshotRunnable ,调用 DisplayPolicy 的 takeScreenshot() 方法,并在其中再调用 ScreenshotHelper 的 takeScreenshot() 。

ScreenshotHelper 的 takeScreenshot() 中,截图请求以及相关信息会被封装一个 ScreenshotRequest ,并通过连接远程服务的方式,使用 Messenger 传给 frameworks-res 中写死的 config_screenshotServiceComponent 对应的服务,这个服务的默认为 com.android.systemui/com.android.systemui.screenshot.TakeScreenshotService

到此为止,运行在 WindowManagerService 的部分结束了,接下来的内容运行在 SystemUI 的上下文。

服务收到请求,对请求进行归类,并拆包取得请求中包含的信息,然后递交给 SystemUI 中的 ScreenshotController 来进一步执行操作。

ScreenshotController 会调用 SurfaceControl 的 captureDisplay() 方法,并最终调用 nativeCaptureDisplay() 走进 native 层中,java 部分到此结束。

ScreenshotController 除了负责转交截图请求外,还负责了截图动画显示、截图完提示窗显示、保存截图之类的操作,我想这是截图操作会通过服务连接的方式交给 SystemUI 完成的核心原因。

离开 java 层后,会进入 android_view_SurfaceControl.cpp 中的 nativeCaptureDisplay() 方法,然后调用 ScreenshotClient 的 captureDisplay() 方法,并最终以 binder ipc 的方式向 SurfaceComposerService 发起调用。

status_t ScreenshotClient::captureDisplay(uint64_t displayOrLayerStack,
                                          const sp<IScreenCaptureListener>& captureListener) {
    sp<ISurfaceComposer> s(ComposerService::getComposerService());
    if (s == nullptr) return NO_INIT;

    return s->captureDisplay(displayOrLayerStack, captureListener);
}

于是,这又是一次进程间通信,运行在 SystemUI 上下文的部分结束了。

那么,SurfaceComposer 的 Bn 端在哪里呢?

诶嘿,就是大名鼎鼎的 SurfaceFlinger 啊。

class SurfaceFlinger : public BnSurfaceComposer,
                       public PriorityDumper,
                       private IBinder::DeathRecipient,
                       private HWC2::ComposerCallback,
                       private ISchedulerCallback

于是,我们进入了系统服务 SurfaceFlinger 的上下文。

接下来的执行路径,便是 SurfaceFlinger 的 captureDisplay() -> captureScreenCommon() -> captureScreenCommon() -> renderScreenImplLocked() ,最后 getRenderEngine().drawLayers() 把截图渲染出来。

好了,既然调用路径已经清晰了,那么就可以看看,有没有什么地方能够隐去截图中的内容了吧。

屏蔽某些 Layer 的渲染

此处不解释 SurfaceFlinger 的工作过程,我们只需要知道 java 层的 Window 反应到 SurfaceFlinger 中就是一个个的 LayerLayer 们根据 z-order 相互叠加和遮盖,渲染后就形成了我们看到的屏幕效果。

也就是说,只需要屏蔽某些 Layer 在截图中的渲染,就可以轻松的做到我们想要的效果。

那么,如何下手呢?

先来看看需要被渲染的 Layer 保存在哪里吧。

status_t SurfaceFlinger::renderScreenImplLocked(
        const RenderArea& renderArea, TraverseLayersFunction traverseLayers,
        const std::shared_ptr<renderengine::ExternalTexture>& buffer,
        bool canCaptureBlackoutContent, bool regionSampling, bool grayscale,
        ScreenCaptureResults& captureResults) {
    ......
    // 要渲染的层信息列表
    std::vector<compositionengine::LayerFE::LayerSettings> clientCompositionLayers;

    ......
    // 描述背景层 即这个区域如果没有任何 layer 覆盖的话会显示为黑色
    compositionengine::LayerFE::LayerSettings fillLayer;
    fillLayer.source.buffer.buffer = nullptr;
    fillLayer.source.solidColor = half3(0.0, 0.0, 0.0);
    fillLayer.geometry.boundaries =
            FloatRect(sourceCrop.left, sourceCrop.top, sourceCrop.right, sourceCrop.bottom);
    fillLayer.alpha = half(alpha);
    // 把背景层加入渲染列表中
    clientCompositionLayers.push_back(fillLayer);

    ......
    // 遍历所有可用 layer
    traverseLayers([&](Layer* layer) {
        ......
        
        compositionengine::LayerFE::ClientCompositionTargetSettings targetSettings{
                clip,
                layer->needsFilteringForScreenshots(display.get(), transform) ||
                        renderArea.needsFiltering(),
                renderArea.isSecure(),
                useProtected,
                clearRegion,
                layerStackSpaceRect,
                clientCompositionDisplay.outputDataspace,
                true,  /* realContentIsVisible */
                false, /* clearContent */
                disableBlurs ? compositionengine::LayerFE::ClientCompositionTargetSettings::
                                       BlurSetting::Disabled
                             : compositionengine::LayerFE::ClientCompositionTargetSettings::
                                       BlurSetting::Enabled,
        };
        std::vector<compositionengine::LayerFE::LayerSettings> results =
                layer->prepareClientCompositionList(targetSettings);
        if (results.size() > 0) {
            ......
            // 把这个层放进渲染列表里
            clientCompositionLayers.insert(clientCompositionLayers.end(),
                                           std::make_move_iterator(results.begin()),
                                           std::make_move_iterator(results.end()));
            ......
        }

    });
    // clientCompositionLayerPointers 来自于对 clientCompositionLayers 的指针萃取
    // 也就是说要渲染的层应该被放进 clientCompositionLayers 里
    std::vector<const renderengine::LayerSettings*> clientCompositionLayerPointers(
            clientCompositionLayers.size());
    std::transform(clientCompositionLayers.begin(), clientCompositionLayers.end(),
                   clientCompositionLayerPointers.begin(),
                   std::pointer_traits<renderengine::LayerSettings*>::pointer_to);

    ......
    // 可以看到,最终描述绘制内容的是 clientCompositionLayerPointers
    getRenderEngine().drawLayers(clientCompositionDisplay, clientCompositionLayerPointers, buffer,
                                 kUseFramebufferCache, std::move(bufferFence), &drawFence);
    ......
}

分析代码的时候是从下往上看的,从 drawLayers() 开始,找出要渲染的东西来自哪里。当然这里为了方便就把分析结果作为注释直接打上去了,省略了分析过程。

这里牵扯到了 traverseLayers() 这个神奇的东西,一眼可能并不能看懂它是个啥,但仿佛有一种 Linux 内核里 for_each_xxx() 宏的感觉,不过还是不太一样的。那就来分析一下这个东西顺便看看 modern cpp 的阴间写法。

首先从参数列表里可以看到 TraverseLayersFunction traverseLayers ,这个东西是个 TraverseLayersFunction 类型的玩意儿。而在 SurfaceFlinger.h 中我们可以找到对这个类型的定义。

using TraverseLayersFunction = std::function<void(const LayerVector::Visitor&)>;

嗯,本质上是个函数指针嘛。
而关于它的参数的类型定义,可以在 LayerVector.h 中找到。

using Visitor = std::function<void(Layer*)>;

诶,也就是说,其实这个 traverseLayers 是一个参数为 void(Layer*) 的高阶函数。

那再来顺着找找这个变量是在哪里被初始化赋值的。

auto traverseLayers = [this, args, layerStack](const LayerVector::Visitor& visitor) {
    traverseLayersInLayerStack(layerStack, args.uid, visitor);
};

可以看到,对它的赋值采用了 lambda 表达式的形式,并在其中调用了 traverseLayersInLayerStack()

void SurfaceFlinger::traverseLayersInLayerStack(ui::LayerStack layerStack, const int32_t uid,
                                                const LayerVector::Visitor& visitor) {
    // We loop through the first level of layers without traversing,
    // as we need to determine which layers belong to the requested display.
    for (const auto& layer : mDrawingState.layersSortedByZ) {
        if (!layer->belongsToDisplay(layerStack)) {
            continue;
        }
        // relative layers are traversed in Layer::traverseInZOrder
        layer->traverseInZOrder(LayerVector::StateSet::Drawing, [&](Layer* layer) {
            if (layer->getPrimaryDisplayOnly()) {
                return;
            }
            if (!layer->isVisible()) {
                return;
            }
            if (uid != CaptureArgs::UNSET_UID && layer->getOwnerUid() != uid) {
                return;
            }
            visitor(layer);
        });
    }
}

循环出现了!
mDrawingState.layersSortedByZ 是一个 SortedVector ,可以理解为按照纵向高度进行排序的 Layer 数组。
这里所作的事情是遍历它,并对所有符合标准的 Layer 执行传入的回调函数。
(这里其实是套了两层,但实际上表达的逻辑应该没有错)

那么事情已经很明确了, traverseLayers() 的作用就是对符合标准的 Layer 们每一个执行一次传入的函数。

而我们上面看到的

    // 遍历所有可用 layer
    traverseLayers([&](Layer* layer) {
        ......
        
        compositionengine::LayerFE::ClientCompositionTargetSettings targetSettings{
                clip,
                layer->needsFilteringForScreenshots(display.get(), transform) ||
                        renderArea.needsFiltering(),
                renderArea.isSecure(),
                useProtected,
                clearRegion,
                layerStackSpaceRect,
                clientCompositionDisplay.outputDataspace,
                true,  /* realContentIsVisible */
                false, /* clearContent */
                disableBlurs ? compositionengine::LayerFE::ClientCompositionTargetSettings::
                                       BlurSetting::Disabled
                             : compositionengine::LayerFE::ClientCompositionTargetSettings::
                                       BlurSetting::Enabled,
        };
        std::vector<compositionengine::LayerFE::LayerSettings> results =
                layer->prepareClientCompositionList(targetSettings);
        if (results.size() > 0) {
            ......
            // 把这个层放进渲染列表里
            clientCompositionLayers.insert(clientCompositionLayers.end(),
                                           std::make_move_iterator(results.begin()),
                                           std::make_move_iterator(results.end()));
            ......
        }

    });

所做的事情便是:为每一个符合标准的 Layer 构建一个 ClientCompositionTargetSettings 结构体,然后执行该 Layer 的 prepareClientCompositionList() 方法来进行一个预处理,并根据返回结果决定是否要把该 Layer 加入到渲染列表中。

预处理的过程就不分析了(因为不是很确定这里会不会牵扯到运行时多态,没时间细看了)。

所以,我们该如何屏蔽一个层呢?

很简单,在传入的函数中一开始就跳过这个层即可。

p1

当然,也可以采用如修改 targetSettings 中的 realContentIsVisible 之类的方法,不过本质上的原理应该是一样的,都是不把这个层放入到 clientCompositionLayers 列表中。

Android 自带的更标准实现

没错,其实系统已经在默默的使用类似的操作了,只不过我一直没有发现。。

在 traverseLayersInLayerStack() 有一个判断

void SurfaceFlinger::traverseLayersInLayerStack(ui::LayerStack layerStack, const int32_t uid,
                                                const LayerVector::Visitor& visitor) {
    ......
    for (const auto& layer : mDrawingState.layersSortedByZ) {
        ......
        layer->traverseInZOrder(LayerVector::StateSet::Drawing, [&](Layer* layer) {
            if (layer->getPrimaryDisplayOnly()) {
                return;
            }
            ......
            visitor(layer);
        });
    }
}
bool Layer::getPrimaryDisplayOnly() const {
    const State& s(mDrawingState);
    if (s.flags & layer_state_t::eLayerSkipScreenshot) {
        return true;
    }

    sp<Layer> parent = mDrawingParent.promote();
    return parent == nullptr ? false : parent->getPrimaryDisplayOnly();
}

eLayerSkipScreenshot 草。

嗯,在 Java 中也有对这个属性的赋值操作,至于属性从 Java 向 Native 层的传递这里就不分析了。
不过可以提一嘴的是,WindowManager LayoutParams 并不会原封不动的传进 Native 里,所以想要借助它传递额外信息还是算了吧。

// SurfaceControl.java
/**
 * Adds or removes the flag SKIP_SCREENSHOT of the surface.  Setting the flag is equivalent
 * to creating the Surface with the {@link #SKIP_SCREENSHOT} flag.
 *
 * @hide
 */
public Transaction setSkipScreenshot(SurfaceControl sc, boolean skipScrenshot) {
    checkPreconditions(sc);
    if (skipScrenshot) {
        nativeSetFlags(mNativeObject, sc.mNativeObject, SKIP_SCREENSHOT, SKIP_SCREENSHOT);
    } else {
        nativeSetFlags(mNativeObject, sc.mNativeObject, 0, SKIP_SCREENSHOT);
    }
    return this;
}

我们只需要操作这个属性,就可以轻松的获得我们想要达到的效果,甚至还有一些额外的 buff ,比如录屏也录不到,外加不会显示非主显示器的任何其它屏幕上(外接显示器时)(投屏应该也投不出来)(简直就是强大极了)。

默认情况下,只有一个东西在使用这个神奇的属性。

// WindowStateAnimator.java
if ((mWin.mAttrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0) {
    flags |= SurfaceControl.SKIP_SCREENSHOT;
}

那就是 “屏幕圆角叠加层” 是一个用来使显示和屏幕边框更加契合的东西。

所以我们只需要

mWindowLayoutParams.privateFlags |=
    WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;

就可以快速获得 “隐身于截图” 的待遇。

当然,这个 Flag 是有鉴权的,应用想要滥用应该是没那么简单的。

总结

这么看了一大圈,还是挺有意思的,牵扯到的东西也挺多。
当然,最后的结论非常简单,什么修改 SurfaceFlinger 都是不需要的。

想要让你的 Window 隐身于截图,就在其 WindowManager.LayoutParams 的 privateFlags 加上 PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY 即可,不仅隐身于截图,还附带隐身于录屏的 buff 。

不过,需要系统级 app 的权限,可能还有隐藏 api 的限制?

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Android 截图时隐藏(穿透)特定的内容