AndroidアプリでWebサーバー(Nanohttpd)を立ち上げる

# はじめに
Androidアプリの中にWebサーバを設置する方法として、Nanohttpdを利用した方法を記載する。

# 1. 開発環境
* macOS High Sierra ver.10.13.6
* node.js: ver.10.15.1
* npm: ver.6.8.0
* yarn: ver.1.13.0
* vue.js (vue cli): ver.3.4.0
* Google Chrome (macOS): ver.74.0.3729.131
* Android Studio: ver. 3.3.1
* Androidスマホ：Moto G5 Plus (Android 8.1.0)

# 2. Kotlinの新規プロジェクトを作る
今回はKotlinベースのAndroidアプリを作る。

## 2-1. 新規プロジェクト作成 (Android Studio)
* Android StudioのFile -> New -> New Project... -> Empty Activity を選択、"Next"をクリック
* 適当な名前をつける。ここでは"AndroidWebApp"とした。
* Languageに"Kotlin"を選択。
* Minimum API level は "API 25: Android 7.1.1 (Nougat)"とした。動作させたいOSに合わせる。
* "Finish"をクリック

## 2-2. 必要なライブラリをインポートする
* Gradle Scripts -> build.gradle (Module: app)のdependenciesに、以下のように`implementation 'org.nanohttpd:nanohttpd-webserver:2.3.1'`を追記。

```gradle:AndroidWebApp/app/build.gradleの一部
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'org.nanohttpd:nanohttpd-webserver:2.3.1'
}
```

* Fileメニュー -> New -> Folder -> Assets Foldersを選択し、assetsフォルダーを作成する。

## 2-3. Webアクセスを有効にする。
AndroidManifest.xmlでACCESS_NETWORK_STATEとINTERNETを有効にする。

```xml:AndroidWebApp/app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="jp.mktshhr.androidwebapp">

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.INTERNET"/>

    <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/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>
```

## 2-4. NanoHTTPDの設定

ここではポート8888で待ち受けるHTTPDを設定する。
NanoHTTPDを継承したWebServerクラスを作り、serveをオーバーライドする。以下にコードをあげるが、必要に応じてMIMEタイプを追加すること。

```kotlin:AndroidWebApp/app/src/main/java/jp/mktshhr/androidwebapp/MainActivity.ktの一部を抜粋
package jp.mktshhr.androidwebapp

import android.content.Context
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import fi.iki.elonen.NanoHTTPD
import java.io.IOException
import java.io.InputStream

class MainActivity : AppCompatActivity() {

    private var context: Context? = null
    private var webServer: WebServer? = null
    private val PORT = 8888

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        this.context = applicationContext
        this.webServer = WebServer(applicationContext)
        try {
            this.webServer?.start()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        if (this.webServer != null) {
            this.webServer?.stop()
        }
    }

    private inner class WebServer(private val context: Context) : NanoHTTPD(PORT) {

        override fun serve(session: IHTTPSession): NanoHTTPD.Response {
            try {
                var uri = session.uri
                if ("/".equals(uri)) {
                    uri = "index.html"
                }

                var filename = uri
                if (uri.substring(0, 1).equals("/")) {
                    filename = filename.substring(1)
                }
                Log.d("AppInfo", filename)

                val asssets = context.getResources().getAssets()
                var fis: InputStream? = null
                try {
                    fis = asssets.open(filename)
                } catch (e: Exception) {
                    Log.d("AppErr", "File openning failed")
                }

                if (uri.endsWith(".ico")) {
                    return NanoHTTPD.newChunkedResponse(Response.Status.OK, "image/x-icon", fis)
                } else if (uri.endsWith(".png") || uri.endsWith(".PNG")) {
                    return NanoHTTPD.newChunkedResponse(Response.Status.OK, "image/png", fis)
                } else if (uri.endsWith(".jpg") || uri.endsWith(".JPG") || uri.endsWith(".jpeg") || uri.endsWith(".JPEG")) {
                    return NanoHTTPD.newChunkedResponse(Response.Status.OK, "image/jpeg", fis)
                } else if (uri.endsWith(".js")) {
                    return NanoHTTPD.newChunkedResponse(Response.Status.OK, "application/javascript", fis)
                } else if (uri.endsWith(".css")) {
                    return NanoHTTPD.newChunkedResponse(Response.Status.OK, "text/css", fis)
                } else if (uri.endsWith(".html") || uri.endsWith(".htm")) {
                    return NanoHTTPD.newChunkedResponse(Response.Status.OK, "text/html", fis)
                } else if (uri.endsWith(".map")) {
                    return NanoHTTPD.newChunkedResponse(Response.Status.OK, "application/json", fis)
                } else {
                    return NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", uri)
                }
            } catch (ioe: IOException) {
                ioe.printStackTrace()
                return NanoHTTPD.newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", ioe.localizedMessage)
            }
        }
    }
}

```

* ビルド(Build -> Make Project)してエラーが出ないことを確認する

# 3. Webアプリのプロジェクト("android-web-app")作成
別途、Webアプリを開発し、Androidアプリに組み込む。
ここではVueのデフォルトページをそのまま利用する。

## 3-1. vue create
* `vue create android-web-app` もしくは `vue ui`でGUI付きセットアップ
 * Preset選択では `default (babel, eslint) ` を選ぶ（そのままリターンすれば良い）
* `cd android-web-app`
 * 特に断りがないときはandroid-web-appフォルダでnpmやyarnなどのコマンドを叩く。 

## 3-2. AndroidアプリへのアクセスによるWebページの表示
* `yarn run build`
 * プロダクション用のhtmlファイル一式をdistフォルダに出力する。  
 * distフォルダの中身をKotlinプロジェクトのassetsフォルダ(AndroidWebApp/app/src/main/assets/)にコピーする。
* Android Studioでビルド(Build -> Make Project)してAndroidスマホにブラウザからアクセス( http://ipaddress:8888/ )する。AndroidスマホのIPアドレスは`adb shell ifconfig`でわかる。
* AndroidスマホのChromeで "http://localhost:8888/" を表示した例 ↓
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123458/b8ba322f-d3c8-6d43-ad7d-5f0237513677.png" width=50%>

# 4. まとめ
vue cli3 を使ったWebアプリのサンプルをAndroidアプリに組み込み、Nanohttpdを使ったWebサーバーにアクセスすることで、スマホやPCのブラウザでWebアプリを表示することができた。