Android WebView 支持H5打开其他App

8,013 阅读4分钟

最近项目中需要实现在WebView中打开的H5页面可以打开其他应用的功能,用本篇文章记录一下该功能的实现方案。

手机自带的浏览器可以通过两种协议来实现打开其他应用,分别为:

  1. Scheme协议(Android Deep Links 基于此协议)。
  2. Intent协议。

1.通过Scheme协议实现

Scheme协议的格式为:

[scheme]://[host]/[path]?[query]

如果你的App中的某个页面需要响应Scheme协议,则需要在Mainfest中添加如下代码:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.minigame.sdktest">

    <application>

        <activity
            android:name="com.minigame.sdktest.ui.HomeActivity"
            android:exported="true">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                //要响应隐式Intent必须提供此类别
                <category android:name="android.intent.category.DEFAULT" />
                //要从浏览器中打开应用必须提供此类别
                <category android:name="android.intent.category.BROWSABLE" />
                
                //代表解析为Activity的URL格式
                <data
                    android:host="www.minigame.vip"
                    android:scheme="https" />
                <data
                    android:host="minigame.vip"
                    android:scheme="jump" />
            </intent-filter>
        </activity>
    </application>
</manifest>

注意:如果在一个intent-filter里面包含了多个data,匹配的规则会涵盖所有data的组合。如果需要匹配唯一的网址,一个intent-filter只能包含一个data。在上面的例子中https://www.minigame.viphttps://minigame.vipjump://minigame.vipjump://www.minigame.vip 都能打开MainActivity。

WebView支持Scheme协议

手机自带的浏览器支持Scheme协议,但是WebView需要自己实现,代码如下:

webView.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(view: WebView?, request:WebResourceRequest?): Boolean {
        val url = request?.url
        val scheme = url?.scheme
        val host = url?.host
        when (scheme) {
            "https" -> {
                //仅过滤某些host进行判断是否跳转,也可不过滤
                if ("www.minigame.vip" == host) {
                    gotoOtherAppBySchemeProtocol(url)
                }
            }
            "jump" -> {
                gotoOtherAppBySchemeProtocol(url)
            }
            else -> {}
        }
        return super.shouldOverrideUrlLoading(view, request)
    }
}

private fun gotoOtherAppBySchemeProtocol(url: Uri) {
//   isActivityExits方法存在限制
//    val intent = Intent(Intent.ACTION_VIEW, url)
//    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
//    if (isActivityExits(intent)) {
//        startActivity(intent)
//    }

    try {
        val intent = Intent(Intent.ACTION_VIEW, url)
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
        startActivity(intent)
    } catch (e: ActivityNotFoundException) {
        //通过直接处理抛出的ActivityNotFound异常来确保程序不会崩溃
        e.printStackTrace()
    }
}

/**
 * intent对应的Activity是否存在
 *
 * 在Android R(11) 以上,根据官方文档(https://developer.android.com/training/package-visibility)
 * 需要通过两种方式来确保有返回值
 * 1.可以在清单文件<queries>标签中添加包名
 * 2.申请QUERY_ALL_PACKAGES权限(文档中描述google对该权限的审核较为严格,可能会导致上架失败)
 *
 * @param intent intent
 */
private fun isActivityExits(intent: Intent): Boolean {
    val resolveActivity = intent.resolveActivity(packageManager)
    return resolveActivity != null
}

默认格式的Scheme(Https、Http)

实现效果如下: originalScheme.gif

可以看到,弹出了一个选择框让用户选择要打开的App,这个效果不是很理想。

官方文档中有提到,在Android M(6.0)以上,如果验证了你是应用和网站的拥有者,那么就不会弹出这个选择框,而是直接进入你的应用。 如果要验证应用和网站的所有权,需要:

  1. 在Mainfest中开启自动验证:
<intent-filter  android:autoVerify="true">
    ...
</intent-filter>

2. 在网站上提供assetlinks.json:

https://domain.name/.well-known/assetlinks.json

assetlink.json文件可以通过AndroidStudio中的App Links Assistant生成,具体步骤参考官方文档

自定义的Scheme

实现效果如下: diyScheme.gif

可以看到直接打开了App。WebView显示无法打开网页的问题,可以根据业务需求来处理,比如停留在上一个页面或者加载一个新的网址。

2.通过Intent协议实现

Intent协议的格式为:

intent://[host]##Intent;package=[String];action=[String];category=[String];component=[String];scheme=[String];S.browser_fallback_url=[String];end;

可以只填需要的参数,比如:

intent://www.minigame.vip##Intent;package=com.minigame.sdktest;scheme=https;S.browser_fallback_url=https://www.minigame.vip;end;

App响应Intent协议所需的配置与Scheme协议相同。

WebView支持Intent协议

手机自带的浏览器支持Intent协议,但是WebView需要自己实现,代码如下:

webView.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(view: WebView?, request:WebResourceRequest?): Boolean {
        val url = request?.url
        val scheme = url?.scheme
        if("intent" == scheme){
            gotoOtherAppByIntentProtocol(url)
        }
        return super.shouldOverrideUrlLoading(view, request)
    }
}

private fun gotoOtherAppByIntentProtocol(url: Uri) {
    val stringUrl = url.toString()
    val fallbackUrl: String = if (stringUrl.contains("S.browser_fallback_url")) {
        stringUrl.substring(stringUrl.indexOf("S.browser_fallback_url"), stringUrl.indexOf(";end"))
    } else {
        ""
    }

    try {
        val intent = Intent.parseUri(url.toString(), Intent.URI_INTENT_SCHEME)
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
        startActivity(intent)
    } catch (e: URISyntaxException) {
        e.printStackTrace()
    } catch (e: ActivityNotFoundException) {
        //通过直接处理抛出的ActivityNotFound异常来确保程序不会崩溃
        e.printStackTrace()
    }
}

实现效果如下: intentProtocol.gif

存在的问题与自定义Scheme一样。

3.通过包名直接打开应用

除了通过协议来打开应用以外,还可以通过JS交互的方式,把要打开的App的包名传递给Android端,通过包名打开应用,代码如下:

//设置是否开启JavaScript
webView.settings.javaScriptEnabled = true
//允许js弹出窗口
webView.settings.javaScriptCanOpenWindowsAutomatically = true

webView.addJavascriptInterface(object : JsInterface {
    @JavascriptInterface
    override fun openApp(packageName: String?) {
        gotoOtherAppByPackageName(packageName)
    }
}, "JsInterface")

fun gotoOtherAppByPackageName(packageName: String?) {
    if (!packageName.isNullOrEmpty()) {
        if (checkInstallStatus(this, packageName)) {
            val intent = packageManager.getLaunchIntentForPackage(packageName)
            if (intent != null) {
                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                startActivity(intent)
            }
        }
    }
}

/**
 * 根据包名判断应用是否安装了
 */
fun checkInstallStatus(context: Context, packageName: String): Boolean {
    return try {
        val packageInfo = context.packageManager.getPackageInfo(packageName, 0)
        packageInfo != null
    } catch (e: NameNotFoundException) {
        e.printStackTrace()
        false
    }
}

实现效果如下: js.gif

总结

上述的几种方案都能满足WebView中打开的H5页面可以打开其他应用的功能,可以按需选用。