Android webview 之onPause暂停audio播放

What

最近,公司里的老司机在帮测试测app的时候,发现一个bug:android端,用webview打开一个活动页,点击活动页中的音乐进行播放,退出webview之后音乐仍在继续播放。

这个bug之前在浏览技术文章的时候有见过,不过当时只是了解了大致的原因,在脑子里做了个问题的索引,并没有索引解决办法。

不过大致方向有了,就和老司机说“这个bug我来解”

英雄气概.gif

Why

网上说这是Android的一个bug!

HOW

网上解决方法有三种

1.在webview所在activity onPause的时候,调用webView的onPause

不过SDK 11之前是不能直接调用webView的onPause,所以此处可以用版本号判断加反射来解决,如果觉得判断版本号麻烦,也可直接用反射。(测试无效/(ㄒoㄒ)/~~)

2.使用pauseTimers()和resumeTimers()

在activity onPause的时候调用webView.pauseTimers(), onResume的时候调用webView.resumeTimers(),暂停/启动本app中所有webview的layout解析和js执行。(测试还是无效 (╯°Д°)╯︵ ┻━┻ )

3.退出的时候载入一个空白的网页

这样退出时有效的,但是按Home键(回到主屏)或者电源键(关闭主屏)还是无效。(ಥ_ಥ)

以下代码来自网络:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private void callHiddenWebViewMethod(String name)
{
if (mWebView != null)
{
try
{
Method method = WebView.class.getMethod(name);
method.invoke(mWebView);
}
catch (NoSuchMethodException e)
{
Log.e("No such method: " + name, e.toString());
}
catch (IllegalAccessException e)
{
Log.e("Illegal Access: " + name, e.toString());
}
catch (InvocationTargetException e)
{
Log.e("Invocation Target Exception: " + name, e.toString());
}
}
}
@Override
protected void onPause() {
super.onPause();
mWebView.pauseTimers();
if (isFinishing())
{
mWebView.loadUrl("about:blank");
setContentView(new FrameLayout(this));
}
callHiddenWebViewMethod("onPause");
}
@Override
protected void onResume() {
super.onResume();
mWebView.resumeTimers();
callHiddenWebViewMethod("onResume");
}

本着 山不过来,我就过去 的思想,想了一会儿(当然不是一直在想这句话-。-),发现js可以暂停audio的播放,而webview又可以执行js代码,豁然开朗啊!
在onPause的时候通过jsInterface来判断网页中是否有audio?是否在播放?在播放就暂停,onResume的时候再恢复。

以下是核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.web_activity);
//...
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
mWebView.loadUrl(
"javascript:var radioTags = document.getElementsByTagName('audio');" +
" var isHtmlAudioPaused = false;" +
"if(radioTags.length > 0) {" +
"try{window.appJs.log('has audio'); window.appJs.hasAudio();}catch(err){}" +
"}"
);
}
@Override
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
Toast.makeText(WebActivity.this, description,
Toast.LENGTH_SHORT).show();
}
});
mWebView.addJavascriptInterface(new HookInterface(this), "appJs");
mWebView.loadUrl(mLoadUrl);
}
@Override
protected void onPause() {
super.onPause();
if(isHasAudio) {
//这里只对页面中只有一个音频的情况做了处理,如果有多个音频需要遍历整个数组记录状态
mWebView.loadUrl(
"javascript:audioEty = document.getElementsByTagName('audio')[0]; " +
"isHtmlAudioPaused = audioEty.paused;" +
"if(!audioEty.paused) {" +
"try{window.appJs.log('to pause audio'); audioEty.pause();}catch(err){}" +
"}"
);
}
mWebView.pauseTimers();
if (isFinishing())
{
mWebView.loadUrl("about:blank");
setContentView(new FrameLayout(this));
}
callHiddenWebViewMethod("onPause");
}
@Override
protected void onResume() {
super.onResume();
mWebView.resumeTimers();
if(isHasAudio) {
mWebView.loadUrl(
"javascript:audioEty = document.getElementsByTagName('audio')[0]; " +
"if(!isHtmlAudioPaused && audioEty.paused) {" +
"try{window.appJs.log('to resume audio'); audioEty.play();}catch(err){}" +
"} else {" +
"window.appJs.log('audio not paused');" +
"}"
);
}
callHiddenWebViewMethod("onResume");
}
public class HookInterface {
private Context mContext;
/** Instantiate the interface and set the context */
public HookInterface(Context c) {
mContext = c;
}
@JavascriptInterface
public void hasAudio() {
//现在只在加载完html之后做了一次是否有音频的检测
//但如果有动态生成的audio Tag,最好不依赖isHasAudio变量
//而是每次进入onPause和onResume的时候都重新检测下
isHasAudio = true;
Log.d("TAG", "this webview has audio");
}
@JavascriptInterface
public void log(String msg) {
if(msg != null) {
Log.i("TAG", "from webview log: " + msg);
}
}
}

PS

以上只处理了单个audio播放的情况,如果有多个audio,需要遍历所有audio进行判断以及注意状态的缓存

Reference

WebView 完全退出flash的方法,停止声音的播放
How to stop youtube video playing in Android webview?
Issue 10282: Public API for WebView.onPause and WebView.onResume