Androidで外部ブラウザを起動する際に考慮したこと

Androidで外部ブラウザを起動させる実装をしました。
単純な実装ではあるのですが、少しはまるポイントがあったのでメモしておきます。



## ブラウザ開くだけ

外部ブラウザを起動する際には、Intentを使用します。
単純に開くだけなら以下のような形になります。
※今回のコードではActivityの拡張関数を作成する形で進めます。

```Activity+Extension.kt
fun Activity.openBrowser(url: String) {
    val intent = Intent(Intent.ACTION_VIEW, url.toUri())
    startActivity(intent)
}
```

これだけで十分ではあるのですが、開けるブラウザアプリがない場合もあります。
Android端末ならChromeなどがデフォルトでインストールされていると思いますが、設定で無効にすることができてしまうためです。
そのためその場合の考慮をします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724617/afdfe276-6595-4632-886a-d07b2550294f.png" width=320>



## ブラウザアプリが存在するかどうかを確認する

渡されたURIを処理するためのアクティビティを知るためには、`queryIntentActivities`または`resolveActivity`を使用します。

```Activity+Extension.kt
fun Activity.openBrowser(url: String) {
    val intent = Intent(Intent.ACTION_VIEW, url.toUri())
    
    val resolveActivity = intent.resolveActivity(packageManager)
    if (resolveActivity != null) {
        startActivity(intent)
    } else {
        Toast.makeText(
            this,
            "有効なブラウザが見つかりません。",
            Toast.LENGTH_SHORT
        ).show()
    }

    // 以下でもよい
    val activities = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
    if (activities.isNotEmpty())
        // 省略
}
```

使用するのはどちらでも良いですが、`queryIntentActivities`の場合は条件を満たすアクティビティをすべて探してきてリストで返し(空ならemptyList)、`resolveActivity`は条件を満たす最適なアクティビティを返します(なければnull)。

ブラウザアプリを開くような場合の動作はほぼ同じで、デフォルトで設定されているブラウザアプリを開きます。なのでどちらでも良いです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724617/3b8e8215-3617-4af6-ad4d-0b4a06b1e5fe.png" width=320>

1点注意として、Android11以降ではほかのアプリの情報を知るPackageManagerの使用が厳格になったため、AndroidManifest.xmlに以下のようにスキーム情報を記載しておくことが必要になります。

```AndroidManifest.xml
<manifest ... >
    <queries>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="http" />
        </intent>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="https" />
        </intent>
    </queries>
    ...
</manifest>
```

これがないと、`queryIntentActivities`や`resolveActivity`が空やnullで返ってきてしまいます。



## はまったポイント

YoutubeやX、Amazonのようなブラウザからディープリンクで専用ネイティブアプリにリダイレクトするようなURLが渡された場合です。

そのアプリがインストールされていなければ問題なくブラウザで開くのですが、インストールされているとうまく開くことができません。
そのため以下のように指定します。

```Activity+Extension.kt
    // MATCH_ALLに変更
    val activities = packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL)
    if (activities.isNotEmpty()) {
        startActivity(intent)
    } else {
        Toast.makeText(
            this,
            "有効なブラウザが見つかりません。",
            Toast.LENGTH_SHORT
        ).show()
    }
```

MATCH_DEFAULT_ONLYは通常使われるアプリだけ探すので基本的に推奨されるのですが、URIからほかのアプリを開く可能性がある場合はMATCH_ALLを使用したほうがよさそうです。

ちなみにこの場合は`resolveActivity`で同じことをするのは難しそうでした。
（他の解決方法があるかもしれません）

これによって、専用アプリがある場合はそちらで、ない場合はブラウザで開くことが可能になりました。



## おまけ：URLが有効な形式かどうか判定

1 . Patterns.WEB_URL

Android標準のutilを使用して、URL形式「っぽい」か判別します。

```Activity+Extension.kt
import android.util.Patterns

fun isValidUrl(url: String) = Patterns.WEB_URL.matcher(url).matches()
```

注意したいのが、このutilは"https://example.com"のようなものはもちろんのこと、"example.com"のようなものも有効なurlだと判定してしまうことです。そこで2です。

2 . スキームでチェックする

httpかhttps形式を持っているのかをチェックすることで、より厳格になります。

```Activity+Extension.kt
import android.net.Uri

fun isValidHttpUrl(url: String): Boolean {
    return try {
        val uri = Uri.parse(url)
        val scheme = uri.scheme
        (scheme == "http" || scheme == "https") && Patterns.WEB_URL.matcher(url).matches()
    } catch (e: Exception) {
        false
    }
}
```

3 . URLクラスでパースできるかを見る

2よりもさらに厳格にみる場合、URLクラスでURLの構文として正しいかをチェックし、ダメそうならMalformedURLExceptionを返すようにするのも良いです。

```Activity+Extension.kt
import java.net.MalformedURLException
import java.net.URL

fun isStrictValidUrl(url: String): Boolean {
    return try {
        val parsedUrl = URL(url)
        parsedUrl.protocol == "http" || parsedUrl.protocol == "https"
    } catch (e: MalformedURLException) {
        false
    }
}
```


以上です。


## 参考

https://qiita.com/ryo_mm2d/items/ab7507237feca0cdda1c