继承AudioManager,实现WebView视频不被其他App音乐中断

648 阅读5分钟

前言

Android15即将release,由于没权限看Android15的源码(除了GPL部分,只有合作厂商可以在release之前看到代码),只好先下载beta版模拟器镜像,导出framework.jar,确认一下AudioManager类是否被final修饰,或者构造方法不再是public。

image.png


我以前做过浏览器项目,直接包装系统WebView组件的那种。如果是照着官方文档调用WebView提供的公开api去做,毫无技术壁垒,市场效果可能还不如新手直接修改开源项目做出的产品。

所以个人追求研究一些与众不同的东西,比如之前博客分享过了的InlineHook实现WebView无损内录

再比如,用户把浏览器放到后台,打开游戏或其他有声App,网页里的视频和音频会暂停。这是用户手机WebView的Chromium内核做的处理,WebView并没有提供控制这个行为的接口,但一样有奇巧淫技能做到。

这个浏览器产品已经没兴趣更新了。接下来就慢慢把一些GPT-4o想不到的思路分享出来。

音频焦点简述

Android系统为了避免不同App同时播放音频,引入了音频焦点的概念。

简单来说,例如App A开始播放音乐之后,用户使用App B播放音乐,那么Android系统就会通知App A“音频焦点丢失”,从而让App A可以做出相应的响应。

了解到这里即可,具体的行为也和Android版本有关,音频焦点更多细节参考文档

WebView的AudioManager

Android的WebView组件源码属于Chromium项目,AOSP中只有编译好的WebView.apk。

WebView也是基于AndroidSDK去开发的,并且他的代码会被加载进App,和普通App一样,它内部要通过Context获取AudioManager,才能调用相关API。可以直接在chromium项目搜索requestAudioFocus,定位到获取AudioManager的相关代码。比如126.0.6429.1版本content\public\android\java\src\org\chromium\content\browser\AudioFocusDelegate.java

private boolean requestAudioFocusInternal() {
    AudioManager am =
            (AudioManager)
                    ContextUtils.getApplicationContext()
                            .getSystemService(Context.AUDIO_SERVICE);

    int result;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        AudioAttributes playbackAttributes =
                new AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_MEDIA)
                        .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
                        .build();
        mFocusRequest =
                new AudioFocusRequest.Builder(mFocusType)
                        .setAudioAttributes(playbackAttributes)
                        .setAcceptsDelayedFocusGain(false)
                        .setWillPauseWhenDucked(false)
                        .setOnAudioFocusChangeListener(this, mHandler)
                        .build();
        result = am.requestAudioFocus(mFocusRequest);
    } else {
        result = am.requestAudioFocus(this, AudioManager.STREAM_MUSIC, mFocusType);
    }

    return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}

扩展AudioManager

有多种修改WebView调用的AudioManager函数参数/返回值的办法,Java层实现Binder Hook是最常见、最成熟的解决方案,也是各种分身/沙箱类产品必备技术。

但是对于这种小问题,还是直接继承AudioManager比较简单,也不用考虑绕过反射黑名单的问题(尽管使用FreeReflection只需要一行代码)。

已经确定了WebView是通过Application获取的AudioManager,我们可以直接重写Application的getSystemService,向WebView返回我们自己的AudioManager子类就好了。如果有需要,可以用调用栈来判断是否来自WebView。

AudioManager有两个构造方法(本文仅考虑API26-API35beta4),分别是无参数和一个context参数。SDK目录里的framework.jar就提供了AudioManager无参构造方法的Stub:

AudioManager() {
    throw new RuntimeException("Stub!");
}

可惜没什么用,因为AudioManager有些方法是需要context的(比如adjustStreamVolume),如果通过无参函数创建AudioManager,还要调用setContext,但setContext对于target34及更高版本的app属于反射黑名单,再去绕过反射限制,倒不如直接代理IAudioService了。

/**
 * @hide
 * For test purposes only, will throw NPE with some methods that require a Context.
 */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public AudioManager() {
}

/**
 * @hide
 */
@UnsupportedAppUsage
public AudioManager(Context context) {
    setContext(context);
}

//...

private void setContext(Context context) {
    mOriginalContextDeviceId = context.getDeviceId();
    mApplicationContext = context.getApplicationContext();
    if (mApplicationContext != null) {
        mOriginalContext = null;
    } else {
        mOriginalContext = context;
    }
    sContext = new WeakReference<>(context);
}

//...

public void adjustStreamVolume(int streamType, int direction, @PublicVolumeFlags int flags) {
    final IAudioService service = getService();
    try {
        service.adjustStreamVolumeWithAttribution(streamType, direction, flags,
                getContext().getOpPackageName(), getContext().getAttributionTag());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

另一方面,framewrok.jar里面的这个构造方法没有标记public,在编译期间,java编译器认为它不能被继承,也就不能成功编译。

所以接下来想办法继承一下带Context类参数的构造方法。

如何继承AudioManager

Android SDK目录的framework.jar,其实就是各种可以让开发者直接调用的类和方法的Stub,而AOSP源码中被@Hide等注解修饰的一些方法不会出现在这个framework.jar中。Github有个叫做"aosp-android-jar"仓库,目的是为了便于开发系统级App更方便调用更多的接口。可以把SDK目录的jar替换为这个项目的jar。

但是,这个项目去除了一些被用于避免空指针的注解@NonNull@Nullable, 很可能导致我们调用系统API时忘了判空,比如android.webkit.WebViewloadUrl本来是声明了传参判空职责交给调用者的:

public void loadUrl(@NonNull String url) {
    checkThread();
    mProvider.loadUrl(url);
}

还有一个办法是直接在项目中创建一个同包名同类名的AudioManager类(其他不存在于SDK的类或者函数,并且无需反射的方法,也可以用这个技巧去调用)。

image.png

可以在gradle进行一些配置避免我们的类打入到APK中。

但是即使打入了APK也无所谓,在App进程创建时已经加载了系统自己的framework.jar的AudioManager,对AudioManager的调用不会调到我们创建的空的AudioManager类。(或者从双亲委派模型的角度考虑,加载AudioManager时,会优先通过父加载器/启动加载器尝试加载)

这样虽然可以正常编译,但AndroidStudio的代码检查,似乎只看得到framework.jar中的AudioManager,所以会把文件标记出语法错误:

image.png

framework.jar文件实际上就是个压缩包,所以我最终的解决方法是,把自己创建AudioManager Stub类编译为class文件,然后用压缩软件,把它替换进framework.jar。

重写关键函数

回到标题,要实现WebView视频不会由于失去音频焦点而中断,只需要让WebView感知不到自己失去了音频焦点就行了,也就是直接拦截换掉焦点回调OnAudioFocusChangeListener。一个最简单的代码如下:

public class MyAudioManager extends AudioManager{

    public MyAudioManager(Context context){
        super(context);
    }

   @Override
    public int requestAudioFocus(AudioFocusRequest focusRequest) {
        AudioFocusRequest build = new AudioFocusRequest.Builder(focusRequest)
                .setOnAudioFocusChangeListener(focusChange -> { })
                .build();
        return super.requestAudioFocus(build);
    }

}

这种思路也可以用到其他各种Service,以及除了WebView的其他第三方库的场景。

上文涉及到的各种做法,或许有更优雅的办法?欢迎评论区补充。