Sesame3にAPIがまだないのでAndroidでAPIサーバーを構築する

追記：2021/02/16 Postで解錠されない不具合修正したv1.0.1アップしました。お手数ですがアップデートお願いします。

Sesame3使ってますか？
Sesame miniに比べるとめちゃくちゃ高速化して最高です。
けどAPIがまだ用意されてないためDIY好きなかたには物足りないですよね。
3月にはAPIの発表があるらしいですが私は待ちきれないためどうにかしてAPIを用意しました。
今回はご家庭にあるいらなくなったAndroid端末を利用して無理くりAPIサーバーを構築する方法をご紹介します。(2000円ほどで中古で買えるAndroid5以降の端末)
APIサーバーになるSesameBridgeというAndroidアプリを作りました。

まずは動いてる動画を
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Sesame3の本番稼働開始！<br>めちゃはや爆速で皆が求めてたこれだよこれがここに。<br>Jermingさんがゲキ速と言ってたのは伊達じゃない。<br>子供達も玄関で待たなくなって喜びそう😄 <a href="https://t.co/BJQ0Lx0ASm">pic.twitter.com/BJQ0Lx0ASm</a></p>&mdash; ode (@odetarou) <a href="https://twitter.com/odetarou/status/1359178824047501315?ref_src=twsrc%5Etfw">February 9, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Sesame3を無理くりWEB API化！<br>要らなくなったAndroid端末(中古2000円程)にWEBサーバーなアプリを自作していれてURLを叩くとNFCのインテントを発行して純正のアプリを起動してトグルする仕組み。<br>3月の公式API出るまでの繋ぎにはやれることとしてのコスパではいいかも？ <a href="https://t.co/asoBFfhrgN">pic.twitter.com/asoBFfhrgN</a></p>&mdash; ode (@odetarou) <a href="https://twitter.com/odetarou/status/1358847940375187459?ref_src=twsrc%5Etfw">February 8, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

Felicaリーダーにタグを読み込ませてAPIを呼んでSesame3を解錠しています。
実行できる種明かしですが、APIが呼ばれたタイミングでSesame公式アプリに対してNFCタグをかざした扱いの命令を出して起動しています。（無理くりです。はい）
# 画面イメージ

アプリ起動後（特に初期設定で使用するのみです）
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132679/e4320d7b-698f-eae9-f6c6-4baa6f6a961a.jpeg">

ブラウザでAndroidのAPIサーバーにアクセスした場合（PCなどからブラウザでSesameを開閉可能です）
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132679/3622d7f8-be43-9617-273c-a9e5a4e2d38b.jpeg">

# 用意するもの
- 玄関におきっぱなしにするAndorid端末。Android5以降でNFCがありSesameアプリが稼働できるのが条件になります。
  - ない場合はメルカリで2000～3000円で買うとよさそうです。私はGalaxy S4(Android5)を利用して問題なく動いています。APIの動作速度的には問題ないのですが、たまにアプリ更新とかでいじるとOS自体が遅い感はあるのでもう少しお金だせそうであれば3000～5000円でNexus5Xが買えそうなのでそちらがいいかもしれません。
- Sesame公式アプリ。NFCタグで起動できる状態にしておいてください。
- Sesame Bridgeアプリ(この後インストール方法を説明します）

# インストール方法

https://github.com/ode1022/sesame_bridge
にあるapkをダウンロードしてインストールして起動します。

初回は権限許可を設定します
"他のアプリの上に重ねて表示できるようにする"
を求められるので許可設定してください。
通常のアプリではこの権限の許可は簡単にしてはいけないやつなので注意ですが今回は特殊な用途なので必要になります。
バックグラウンドで動いている最中にもSesameアプリを起動するために必要な権限になります。
権限周りの話は後述します。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132679/010951da-b560-b16e-7972-f44e3e7214fd.jpeg">

アプリ起動後に設定ボタンを押して
NFCのUIDを設定します。
Sesame3に付属のNFCタグをかざすことで読み取って設定できます。
Sesame公式アプリで認識させているNFCタグのを設定してください。
これでAPIを呼び出す準備は完了です。

試しに`http://AndroidのIP:5000`をブラウザでアクセスしてください。
アプリの真ん中に表示されるIPを押すと開けます。

ページ真ん中に"Sesameトグル開閉"というボタンが表示されるので
そちらを押すとSesame公式アプリが起動して鍵の開閉が行われます。

実際にAPIを呼び出す場合は
`http://AndroidのIP:5000`
にPostすることで実行可能です。

なおこのアプリはフォアグラウンドサービスとして起動しています。
通知バーに常駐してスワイプでは消せないあれになります。（邪魔なら通知設定で消すことは可能ですけど）
この仕組みによりSesame公式アプリが起動している状態でもバックグラウンドでAPIサーバーを常駐しておけます。



合わせてAndroid端末のスリープ防止の対策をしておくといいかもしれません。
Androidのスリープ設定を極めよう！設定時間/スリープさせないのは可能？ | Aprico
https://aprico-media.com/posts/1860
開発者設定にありました。

# API活用

下記記事にてESP32と連携してFelica Lite-Sタグで安全に子供でも利用できるシステムを構築しましたのでよろしければ参考にしてみてください。（今回はこれを改修してSesameBridgeのAPIをコールするようにしました）

Suica(IDm)で認証するのは危険なのでFelica Lite-Sの内部認証を使う
https://qiita.com/odetarou/items/bcd65dbfd1f68735ac30

# 注意点

特にAPIの認証系は作っていませんのでWifiにアクセスできる人は信頼できる人のみの前提になります。
必要に応じて改修してみてください。Pull Request歓迎です！

NFC起動でSesame公式アプリがクラッシュする場合がありました。
手動でクラッシュダイアログを閉じる必要があるのでその間はAPIが呼べなくなりました。
2021/2/11あたりのアプリ更新履歴にクラッシュするのを直したと書いてあったので最新版を入れれば安定するかもしれません。

# ひとこと
おそらくこのアプリの寿命は2,3ヶ月でしょう。。
3月に公式APIが発表されれば不要のはずですが、恐らく発表のみで実際に利用できるのはそれ以降かも。
またBluetooth APIもESP32などのマイコン等では当初は利用できない形ではないかという予想もありもう少し延命になる可能性も考えています。
早くこのアプリがいらなくなるといいですね。

以降はこのアプリを開発して調べた技術解説になります。こちらのほうが寿命が長いメインコンテンツかもしれません（笑）

# 技術解説

## バックグラウンドから他アプリのActivityをインテントで起動する方法

### 権限よもや話

Android6以降ではSYSTEM_ALERT_WINDOW"他のアプリの上に重ねて表示できるようにする"権限が許可制になりました。5以前は許可不要で他アプリの上に描画できてしまうのが気軽に使用できるという恐ろしい時代でした。

Android8以降ではフォアグラウンド サービスとして通知バーに常駐させないとバックグラウンドで処理ができなくなりました。
Android7以前では権限や通知表示がなくてもバックグラウンドから他アプリを起動できるという恐ろしい時代でした。子供がタブレットに適当にいれていた謎のゲームがバックグラウンドで急に広告をブラウザで起動させるなどしていて治安が悪かったです。。
バックグラウンド実行制限  |  Android デベロッパー  |  Android Developers
https://developer.android.com/about/versions/oreo/background

Android10以降ではバックグランドからActivityを起動することはフォアグラウンド サービスを使用してもできなくなりました。
バックグラウンドからのアクティビティの起動に関する制限  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/components/activities/background-starts
>アクティビティを開始するという点では、フォアグラウンド サービスを実行中のアプリも“バックグラウンドにある”と見なされます。

### Android10以降に対応したバックグランドからの起動方法

foreground service - How To Start An Activity From Background in Android 10? - Stack Overflow
https://stackoverflow.com/questions/58245398/how-to-start-an-activity-from-background-in-android-10
回避するにはSYSTEM_ALERT_WINDOW権限を使うしかなさそうです。("他のアプリの上に重ねて表示できるようにする"の権限のこと)

SYSTEM_ALERT_WINDOW権限の取得方法は下記記事が参考になりました。（特に今回はActivity起動のみでViewは作成していません）
常に画面の最前面に表示されたままになる View を作る (TYPE_APPLICATION_OVERLAY) | まくまくAndroidノート
https://maku77.github.io/android/ui/always-top.html

## NFCのインテントをエミュレート(モック）してSesameアプリを起動する裏技

海外の記事になりますが紹介です。
android - Testing an app by faking NFC tag scan - Stack Overflow
https://stackoverflow.com/questions/34882171/testing-an-app-by-faking-nfc-tag-scan
普通にNFCのインテントを投げることはできるのですが相手側アプリでtagオブジェクトを使用することはできないとのことでした(通常はリーダーライターからtagオブジェクトを取得するため)。tagを利用できるようにする場合はmockを作るひつようがあると下記３つほど過去事例のurlが紹介されていました。

How to mock a Android NFC Tag object for unit testing - Stack Overflow
https://stackoverflow.com/questions/30841803/how-to-mock-a-android-nfc-tag-object-for-unit-testing

android - Is there a way to create an ACTION_NDEF_DISCOVERED intent from code - Stack Overflow
https://stackoverflow.com/questions/26712905/is-there-a-way-to-create-an-action-ndef-discovered-intent-from-code

android - How to simulate the tag touch from other application - Stack Overflow
https://stackoverflow.com/questions/28046115/how-to-simulate-the-tag-touch-from-other-application

https://github.com/ode1022/sesame_bridge/blob/eccedf1ff2fbcb13a2759c772ed425636aa793f6/app/src/main/java/jp/ode/sesamebridge/MyService.kt#L135-L176
下記のコードになります。ndefメッセージはAndroidアプリのNFC Toolsをいれて確認した値になります。

```.kotlin
        val ndefMessage = NdefMessage(
            NdefRecord.createTextRecord("ja", "candynfc")
        );

        val tagClass = Tag::class.java
        val createMockTagMethod = tagClass.getMethod(
            "createMockTag",
            ByteArray::class.java,
            IntArray::class.java,
            Array<Bundle>::class.java
        )

        val TECH_NFC_A = 1
        val TECH_NDEF = 6

        val EXTRA_NDEF_MSG = "ndefmsg"
        val EXTRA_NDEF_CARDSTATE = "ndefcardstate"
        val EXTRA_NDEF_TYPE = "ndeftype"

        val ndefBundle = Bundle()
        ndefBundle.putInt(EXTRA_NDEF_MSG, 48) // result for getMaxSize()
        ndefBundle.putInt(EXTRA_NDEF_CARDSTATE, 1) // 1: read-only, 2: read/write
        ndefBundle.putInt(
            EXTRA_NDEF_TYPE,
            2
        ) // 1: T1T, 2: T2T, 3: T3T, 4: T4T, 101: MF Classic, 102: ICODE
        // https://www.sony.co.jp/Products/felica/NFC/forum.html
        // を見ると
        // >Type 2はMIFARE Ultralight®（NXP社）
        // 2でいいみたい。
        ndefBundle.putParcelable(EXTRA_NDEF_MSG, ndefMessage)
        val mockTag = createMockTagMethod.invoke(
            null, id, intArrayOf(TECH_NFC_A, TECH_NDEF), arrayOf(null, ndefBundle)
        ) as Tag

        intent.putExtra(NfcAdapter.EXTRA_TAG, mockTag)
        intent.putExtra(NfcAdapter.EXTRA_ID, id)
        intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, arrayOf(ndefMessage))
        intent.setType("text/plain")
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
        startActivity(intent)
```

mock作るやりかたはcreateMockTagという隠しメソッドをリフレクションで呼んでいます。
targetSdkVersion 28(Android9)では動かなくなったため
targetSdkVersion 27(Android8.1)で実行する必要がありました。
他の回避策は調べてもわからずでした。
何かいい方法があればいいのですが現状は古いAPIに引きずられるためStoreに申請するようなアプリには利用できなそうです（targetSdkVersionを最新にする必要があるため）
そもそも非公開APIですし。。

## LAN内のIPアドレスを取得する方法

ソースの下記らへんが参考になります。
https://github.com/ode1022/sesame_bridge/blob/eccedf1ff2fbcb13a2759c772ed425636aa793f6/app/src/main/java/jp/ode/sesamebridge/MainActivity.kt#L61-L88

```.kotlin
 private fun getIPAddress(): List<String> {
        var resultAddresses = mutableListOf<String>()
        val interfaces: Enumeration<NetworkInterface> = NetworkInterface.getNetworkInterfaces()
        while (interfaces.hasMoreElements()) {
            val network: NetworkInterface = interfaces.nextElement()
            val addresses: Enumeration<InetAddress> = network.getInetAddresses()
            while (addresses.hasMoreElements()) {
                val address = addresses.nextElement()
                // IPv4でなければ無視
                if (!(address is Inet4Address) ) {
                    continue
                }
                // ループバックアドレスは無視
                if ( address.isLoopbackAddress() ) {
                    continue
                }
                //resultAddresses.add(network.name)
                // セルラー接続(モバイルデータ)などは無視
                // https://stackoverflow.com/questions/33747787/android-networkinterface-what-are-the-meanings-of-names-of-networkinterface
                if ( network.name.startsWith("rmnet")) {
                    continue
                }

                resultAddresses.add(address.hostAddress)
            }
        }
        return resultAddresses
    }
```
単純にIPを取得しようとするとIPV6のアドレスやループバックアドレス、モバイルデータ通信のアドレス等も含まれるためフィルタリングする必要がありました。

network interface - Android NetworkInterface. What are the meanings of names of NetworkInterface? - Stack Overflow
https://stackoverflow.com/questions/33747787/android-networkinterface-what-are-the-meanings-of-names-of-networkinterface

## バックグラウンドでもAPIサーバーを起動しておく方法

通常他アプリに切り替えた場合等にはアプリはバックグラウンドになるため権限話でも記載したようにAndroid8以降では処理ができなくなります。
回避策としてフォアグラウンド サービスとして通知バーに常駐させることで回避できます。（正しくはフォアグラウンドサービスを作る条件として通知を表示し続ける必要がある形になります）

実装は下記記事を参考にしました。

AndroidアプリでForeground Serviceを使って、画面スリープ状態でも位置情報を定期取得する | DevelopersIO
https://dev.classmethod.jp/articles/android-use-foreground-service-for-location-background/

Foreground Serviceの基本 - Qiita
https://qiita.com/naoi/items/03e76d10948fe0d45597

[Android] Service の使い方
https://akira-watson.com/android/service.html

## AndroidでAPIサーバーを構築する方法

3つほどライブラリを見つけました。
今回はシンプルな実装のためAndroidAsyncを利用しました。

### AndroidAsync
https://github.com/koush/AndroidAsync
HTTPクライアントとサーバーを兼ねてるので専用感はないですがシンプルに実装できるのが良かったです。
github starが多いのと更新がactiveなのもポイントでした。
最終更新日2020/12/21（2021/02/15時点にて）

公式サンプル引用

```.java
AsyncHttpServer server = new AsyncHttpServer();

List<WebSocket> _sockets = new ArrayList<WebSocket>();

server.get("/", new HttpServerRequestCallback() {
    @Override
    public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
        response.send("Hello!!!");
    }
});

// listen on port 5000
server.listen(5000);
// browsing http://localhost:5000 will return Hello!!!
```

今回実装した箇所(postやquerystringを処理するなどしています)
https://github.com/ode1022/sesame_bridge/blob/eccedf1ff2fbcb13a2759c772ed425636aa793f6/app/src/main/java/jp/ode/sesamebridge/MyService.kt#L30-L73

### NanoHTTPD 
https://github.com/NanoHttpd/nanohttpd
より細かく実装する場合はNanoHTTPDがメジャーそうでした。
ただし若干activeではなさそうです。
最終更新日2019/7/4（2021/02/15時点にて）
日本語の記事もそこそこあるのは良さそうです。

Android内にHttpサーバを立てて、ChromeCastでローカルファイルを再生する - Qiita
https://qiita.com/sjnya/items/dcb58ffdfcd2013fb159

AndroidアプリでWebサーバー(Nanohttpd)を立ち上げる - Qiita
https://qiita.com/mktshhr/items/9effdd4a8fa9616095d6

NanoHTTPDで始めるAndroid機のリモート操作
https://www.slideshare.net/i_Pride/nanohttpdandroid

### Android HTTP Server
https://github.com/piotrpolak/android-http-server
とても重厚感があり、複雑なことをやる場合に採用とかでしょうか。
ややこしい仕様とServletのインターフェースを採用ということで無しでした。。
最終更新日2020/2/6（2021/02/15時点にて）