Androidプロジェクト内のWebviewをデバッグする

## はじめに

Webviewが表示されなくなったから見て欲しいと依頼されることがある。
Webviewだけで完結するものでよければいいのだが、ログインのtoken情報をアプリから受け取ったりすることが想定されるアプリでは、
ページを動かしただけでは、画面が表示されない場合がある。
どのようなやりとりをされているのかドキュメントが残っていればいいのだが、
昔の作ったもので、埋め込んだあと特に変更されていないものの場合、
ソースコードを頼りに、目星をつけるか、ネイティブアプリのエンジニアに聞く必要がある。テストコードがあればそこから目星をつけることも可能だが
また、tokenは、常に使えるものでもないので、定期的に更新するのも大変である。
なので、アプリのWebviewをそのままデバッグできるところまで、確認できたものを備忘録として残しておく。

本記事は、Projectも何もないまっさらな状態からサンプルアプリを作成して、デバッグするまでの流れで記述しています。
既存のWebViewのProjectがあり、どの辺の修正を加えればデバッグ可能か知りたい場合は、[AndroidのWebviewのURLをLocal環境に設定する](#androidのwebviewのurlをlocal環境に設定する)まで飛ばしてください。

## Webviewアプリを作成する

Android Studio（筆者の環境では4.1.1）で、新規プロジェクトを作成する。
テンプレートを使っても良いとは思うが、本記事では、「基本アクティビティー(Basic Activity)」を使用する。

確認ようのアプリなので、プロジェクトの構成は以下のようにした。
言語 : Kotlin
最小 SDK : API 29: Android 10.0 (Q)

※ デバイスの追加については省略します。 Android 4.4（KitKat）以降が対象となります。

まずはじめに、インターネットに接続するための、パーミッションを追加する。

```diff:app/src/main/AndroidManifest.xml
      </application>
+     <uses-permission android:name="android.permission.INTERNET"/>
  </manifest>
```

画面にWebviewを設置する。
使用したテンプレートの基本アクティビティーはフラグメントに設定させているので、`app/src/main/res/layout/fragment_second.xml`にWebviewを設置する。
※ MainAcrivityのみしかないテンプレートを選択したときは、MainAcrivityに記述することになるが、内容が異なるので注意が必要

余計な`textview_second`のコンポーネントを削除を行い、
`app/src/main/res/layout/fragment_second.xml`パレットから「Widgets」-「Webview」をドラックアンドドロップで設置する
idをwebviewとする

![スクリーンショット 2020-12-18 18.55.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/fddfcad1-9bc8-b522-3049-df97313817a7.png)

次にWebviewの設定内容を`app/src/main/java/com/example/webviewapp/SecondFragment.kt`に記述していく
最初ということで、`https://example.com/`のサンプルページを表示させるようにする。

```kotlin:app/src/main/java/com/example/webviewapp/SecondFragment.kt
class SecondFragment : Fragment() {

    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        val view: View =  inflater.inflate(R.layout.fragment_second, container, false)

        //xmlのWebViewで指定したid
        val myWebView: WebView = view.findViewById(R.id.webview)
        myWebView.loadUrl("https://example.com/")

        return view
    }
```

Android Emulator上でアプリを実行する。

![スクリーンショット 2020-12-19 1.14.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/12b68353-03a2-40a6-b9c9-ed05c587b254.png)
上記のようにExample DomainのページをWebviewで表示できれば成功。


## Webviewで表示させるコンテンツを作成する

コンテンツ提供のためのWebサーバを作成する。
簡易的なものをgolangで作成する。

```golang:main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"text/template"
)

type Page struct {
	Title string
}

func handler(w http.ResponseWriter, r *http.Request) {

	switch r.Method {
	case http.MethodPost:
		page := Page{"Webview Test"}
		tmpl, err := template.ParseFiles("layout.html")

		if err != nil {
			http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
			return
		}

		tmpl.Execute(w, page)
	}

}

func main() {
	var httpServer http.Server

	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static/"))))
	http.HandleFunc("/", handler)

	log.Println("start http listening :8080")
	httpServer.Addr = ":8080"
	log.Println(httpServer.ListenAndServe())
}
```

```html:layout.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>{{.Title}}</title>
    <link rel="stylesheet" href="/static/index.css">
  </head>
  <body>
    <div id="wrapper" class="hiddden">
      <h1>ようこそ</h1>
      <h3 id="id">ID</h3>
      <h3 id="name">表示名</h3>
    </div>
    <script src="/static/main.js"></script>
  </body>
</html>
```

初期状態では、非表示なるようにcssを作成する。
他、細かいstyleの調整をする。

```css:static/index.css
body {
    display: flex;
    min-height: 100vh;
    margin: 0;
}

#wrapper {
    margin: auto;
    text-align: center;
}

#wrapper.hidden {
    display: none;
}
```


AndroidからWebviewに、値を渡すための関数を用意する。

```javascript:static/main.js
function setValue(id, name) {
    if (arguments.length !== 2) {
        throw Error('2つ引数を持つ関数です');
    }

    console.log(`id : ${id}`)
    console.log(`name : ${name}`)

    const idElement = document.getElementById('id');
    idElement.textContent = id;

    const nameElement = document.getElementById('name');
    nameElement.textContent = name;

    const wrapper = document.getElementById('wrapper');
    wrapper.classList.remove('hidden');
}
```

```bash
$ go run main.go
```


## AndroidのWebviewのURLをLocal環境に設定する。

まず、Webviewの向き先をlocalサーバに変更する。


```kotlin:app/src/main/java/com/example/webviewapp/SecondFragment.kt
    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        val view: View =  inflater.inflate(R.layout.fragment_second, container, false)

        val myWebView: WebView = view.findViewById(R.id.webview)
        myWebView.settings.javaScriptEnabled = true

        WebView.setWebContentsDebuggingEnabled(true)

        val postData = ""
        myWebView.postUrl("http://10.0.2.2:8080/", postData.toByteArray());

        val id = "1234567890"
        val name = "test"

        myWebView.webViewClient = object : WebViewClient() {
            override fun onPageFinished(webView: WebView, url: String) {
                super.onPageFinished(webView, url)

                val script = "window.setValue('$id','$name')"
                println(script)

                webView.evaluateJavascript(script, null)
            }
        }
```


`WebView.setWebContentsDebuggingEnabled(true)`はChrome Developer Toolsを許可するオプション。
Webviewのインスタンスではなく、staticメソッドからの設定となるので注意が必要。
`myWebView.settings.javaScriptEnabled = true`はデフォルトでは、JavaScriptが有効化されないので、有効化する。
最近では、SPAのWebviewも多くなってきていると思うので、つけることが多いのかもしれない
`10.0.2.2`はエミュレータから開発ホストへのループバックインターフェースへのエイリアスで、開発端末のlocalhostにあたる。
`webView.evaluateJavascript(script, null)`で特定のJavaScriptをネイティブアプリから実行させることができる。

![スクリーンショット 2020-12-19 18.10.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/960936d3-2ce1-b869-0141-699e8300b59a.png)

次に、ローカル実行しているサーバはHTTPSではないので、

```xml:/app/src/main/res/xml/network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain incincludeSubdomainsludeSubdomains="false">10.0.2.2</domain>
    </domain-config>
</network-security-config>
```

設定ファイルを読み込ませようにAndroidManifest.xmlを修正する。

```diff:app/src/main/AndroidManifest.xml
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
-       android:theme="@style/Theme.WebviewApp">
+       android:theme="@style/Theme.WebviewApp"
+       android:networkSecurityConfig="@xml/network_security_config">
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/Theme.WebviewApp.NoActionBar">
```

![スクリーンショット 2020-12-19 18.10.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/ceedf180-51a3-5c1c-c1c6-dc4d6f6ede76.png)
上のような画面が表示できれば成功

※ 以上の設定をして、再ビルドしても、「Webpage not available」のエラーページが出る場合は、エミュレーターの対象アプリを一度アンインストールして、再度ビルドすることで表示されるようになるかもしれません。私はこれで、なおりました。

## Chrome Developer Toolsで確認する

エミュレーターでWebviewを表示させる。
Google Chromeのアドレスバー`chrome://inspect`を入力する。

Deveicesタブから
Webview in XXXX というようにエミュレータで表示されているWebviewが選択可能になっているので、inspectをクリックする。


![スクリーンショット 2020-12-19 18.23.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/f754c52a-c53c-5595-ab89-539575f1f22c.png)

ネイティブアプリから呼び出した、JavaScriptのLogもしっかり出力されており、ElementsタブからWebviewのDOM構成も確認することが書きます。
Chromeから更新で行っても、ネイティブで継承した、onPageFinishedはちゃんと呼び出されているようなので、安心です。

![スクリーンショット 2020-12-19 18.27.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232364/953f7bf5-2b9c-40c4-8d88-a6095a7ab37b.png)



## 
の内容を押さえれば、ソースコードがあること前提になりますが、Webviewを通常のwebアプリケーションと同様に、デバッグができるかと思います。

エミュレータの実行は、多くのメモリの消費するようですので、PCのスペックも確認しておくことをお勧めします。


## 参考
- https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews?hl=ja
- https://developer.android.com/studio/run/emulator-networking?hl=ja