Help us understand the problem. What is going on with this article?

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'を追記。
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を有効にする。

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タイプを追加すること。

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/" を表示した例 ↓

4. まとめ

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

mktshhr
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away