20200321のAndroidに関する記事は6件です。

Lapack(clapack)を Android arm64 + cmake + NDK r20 or later でビルドする

背景

  • Lapack(+ BLAS) を使うアプリを Android でビルドせねばならない.
  • CMake + NDK r20 or later(clang)でビルドしたい

ビルド

https://github.com/syoyo/clapack-cmake-android-arm64

に上げました.

clapack-3.2.1-cmake をベースにしています.

いくらか warning 出ますが, build できます.

aarch64 linux(Jetson AGX)で unit test を走らせたら, xeigtstz_*** のテストは実行がコケました.

変更したところ

uninit.c

FPU の設定をします.
使っていないようなので取り除きました.
(Android では FPU control できるかな?)

__off64_t

sysdep1.h で, OFF_T を int64_t にしました.

arith.h

f2clib で, 実行時にシステムの情報を取得して arith.h を作っています.
cross compile だとコケますので, aarch64 linux(Jetson AGX)ででっち上げました.

TODO

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Fuelを使ってパラメータ付きPOSTリクエストを送信した

なんとかHTTPリクエストをAndroidのエミュレーターからローカルホストのLaravelサーバーに送信することができたので投稿します。

ライブラリのインストール

app/build.gradleにインストールするライブラリを書いていきます。

app/build.gradle
dependencies {
    // Fuel for HTTP Connections
    implementation 'com.github.kittinunf.fuel:fuel:2.2.0'
    implementation 'com.github.kittinunf.fuel:fuel-gson:2.2.0'

    implementation "com.squareup.moshi:moshi:1.5.0"
    implementation "com.squareup.moshi:moshi-kotlin:1.5.0"
}

HTTPリクエストを送信するためのFuelと,JSONのパラメータを作成するためのmoshiをインストールします。
(僕は最初,app/build.gradleじゃなくてルートのgradleにimplementationを書いちゃって動きませんでした...。)

パラメータのフォーマットを決める

JSONのパラメータのフォーマットを決めるためのクラスSampleRequestFormatクラスを作成します。

SampleRequestFormat.kt
class SampleRequestFormat(val kekey: String)

kekeyという名前のキーを定義しました。

Androidの画面作成

画面にボタンが一つ配置されているだけのものです。

image.png

activity_debug.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/post_debug_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button" />
</LinearLayout>

リクエスト送信用のアクティビティを作成

POSTでLaravelのサーバーへリクエストをパラメータ付きで送るためのプログラムを作成します。

DebugActivity.kt
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.live_barcode_reader.request_format.SampleRequestFormat
import com.github.kittinunf.fuel.Fuel
import com.squareup.moshi.KotlinJsonAdapterFactory
import com.squareup.moshi.Moshi

class DebugActivity : AppCompatActivity() {
    private var button: Button? = null
    private val URL: String = "http://10.0.2.2:8000"
    private val domain: String = "/sample9999"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_debug)
        val targetURL: String = URL + domain

        val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
        val requestAdaper = moshi.adapter(SampleRequestFormat::class.java)

        button = findViewById(R.id.post_debug_button)
        button?.setOnClickListener {
            val header: HashMap<String, String> = hashMapOf("Content-Type" to "application/json")
            val sampleRequest = SampleRequestFormat(
                kekey = "from Android"
            )

            Fuel.post(targetURL)
                .header(header)
                .body(requestAdaper.toJson(sampleRequest))
                .response { _, response, result ->
                    println(response)
                    println(result)
                }

            //Toast.makeText(this, "PUSHED", Toast.LENGTH_SHORT).show()
        }
    }
}

このとき,URLの中身がhttp://10.0.2.2:8000であることを覚えておいてください!
最後に,HTTP通信の設定についてAndroidManifest.xmlに記述します。

HTTP通信の設定

network_security_config.xmlというファイルを作成し,HTTP通信できるドメインを指定して許可します。

network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:android="http://schemas.android.com/apk/res/android">
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">10.0.2.2</domain>
    </domain-config>
</network-security-config>

その後,AndroidManifest.xmlにも変更を加えます。

AndroidManifest.xml
<application
    ...
    android:networkSecurityConfig="@xml/network_security_config">

次はLaravelのサーバーです。

サーバーサイド

web.php
Route::post('/sample9999', 'TestController@sample9999');

sample9999というPOSTリクエストに反応してTestControllerの中のsample9999()という関数が反応するようにします。9999という数字は少し理由があって付けました。

TestController.php
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class TestController extends Controller
{
    public function sample9999(Request $request) {
        $json_string = file_get_contents('php://input');
        logger($json_string);
        $json_object = json_decode($json_string);
        logger(var_export($json_object, true));
        logger(gettype($json_object));

        logger($json_object->kekey);

        return 'got request';
    }
}

とりあえずこのコードで動作確認だけしてみました。

動作確認

Androidとサーバーを起動して,エミュレータのボタンを押すと,

laravel.log
[2020-03-21 08:59:49] local.DEBUG: {"kekey":"from Android"}  
[2020-03-21 08:59:49] local.DEBUG: (object) array(
   'kekey' => 'from Android',
)  
[2020-03-21 08:59:49] local.DEBUG: object  
[2020-03-21 08:59:49] local.DEBUG: from Android  

Androidのログ↓

I/System.out: <-- 200 http://10.0.2.2:8000/sample9999
    Response : OK
    Length : -1
    Body : got request
    Headers : (11)
    Cache-Control : no-cache, private
    Connection : close
    Set-Cookie : laravel_session=eyJpdiI6IkVrR0RxK241U0xkWkRUbFhWWmhMSXc9PSIsInZhbHVlIjoicEF2d2h5dXVDVFdmVElDc09hLzNvMFJmNmM2bitBbTRrSFBBQjVucEN1elY5eGNUQy9ZZUFqVkExdkplTGEwWSIsIm1hYyI6IjAzMTBiZWEwN2FlYmYxODhlZWQ3OWY2MmU1YTBhMWQ2MWNiYzIzMGI3ZWY1NWM0NjlhYTNkMzkxMzJiZTgyYzIifQ%3D%3D; expires=Sat, 21-Mar-2020 10:59:49 GMT; Max-Age=7200; path=/; httponly; samesite=lax
    Date : Sat, 21 Mar 2020 08:59:49 GMT, Sat, 21 Mar 2020 08:59:49 GMT
    X-Android-Selected-Protocol : http/1.1
    X-Powered-By : PHP/7.3.11
    Content-Type : text/html; charset=UTF-8
    Host : 10.0.2.2:8000
    X-Android-Received-Millis : 1584781188564
    X-Android-Response-Source : NETWORK 200
    X-Android-Sent-Millis : 1584781188475
    [Success: [B@615227d]

HTTPリクエストが送られています!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

OpenBLAS を Android aarch64 + NDK r20 or later でビルドするメモ

背景

OpenBLAS(+ Lapack)を Android arm64 で動かしたい.

Lapack(clapack)はこちら

Lapack(clapack)を Android arm64 + cmake + NDK r20 or later でビルドする
https://qiita.com/syoyo/items/09009ca77393cc0bd986

ビルド方法

How to build OpenBLAS for Android
https://github.com/xianyi/OpenBLAS/wiki/How-to-build-OpenBLAS-for-Android

は情報が古いです.

NDK r20 などでは, gcc 関連がそれなりに消えて clang only になったので, wiki の情報だとビルドがうまくいきません.

Compiling OpenBLAS for Android #2005
https://github.com/xianyi/OpenBLAS/issues/2005

にありました.

cd $OPENBLAS_SOURCE
export NDK=${HOME}/Android/sdk/ndk-bundle
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64
make \
    TARGET=CORTEXA57 \
    ONLY_CBLAS=1 \
    CC=$TOOLCHAIN/bin/aarch64-linux-android21-clang \
    AR=$TOOLCHAIN/bin/aarch64-linux-android-ar \
    HOSTCC=gcc \
    -j4
make PREFIX=./android-openblas/arm64 install

でビルドできます! sysroot 指定など不要でビルド楽です.

CORTEX architecture

現時点(2020/03/21)では, CORTEXA53, 57, 72, 73 がありました.

Cmake build

ソースコードのレイアウト上, out-of-tree build はちょっと気持ち悪いことがわかりました
(生成された congig.h ではなく, 既存の config.h(x86 用のデフォルト設定) をインクルードしてしまうため, config.h を消さないといけない)

in-source ビルドではこんな感じでいけました.

https://github.com/syoyo/OpenBLAS/blob/develop/scripts/bootstrap-android-cmake.sh

## Please edit android sdk/ndk/cmake path
ANDROID_SDK_ROOT=$HOME/Android/Sdk/
ANDROID_NDK_ROOT=$HOME/Android/Sdk/ndk-bundle
#ANDROID_NDK_ROOT=$HOME/Android/Sdk/ndk/android-ndk-r21/
# CMake 3.10 or later required
CMAKE_BIN=$ANDROID_SDK_ROOT/cmake/3.10.2.4988404/bin/cmake
#CMAKE_BIN=cmake

# For ninja build
# -DCMAKE_MAKE_PROGRAM=$ANDROID_SDK_ROOT/cmake/3.6.4111459/bin/ninja \

# Due to the source code layout of OpenBLAS, we need to use in-source build 
# Otherwise compilation fails due to the conflict of existing `config.h` and generated `config.h`
# (Using in-source build correctly overrides `config.h`)

$CMAKE_BIN \
  -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_ROOT/build/cmake/android.toolchain.cmake \
  -DANDROID_ARM_MODE=arm \
  -DANDROID_ARM_NEON=On \
  -DANDROID_ABI=arm64-v8a \
  -DANDROID_NATIVE_API_LEVEL=28 \
  -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" \
  -DANDROID_STL=c++_shared \
  -DTARGET=CORTEXA73 \
  -DNOFORTRAN=1 \
  -DDYNAMIC_ARCH=0 \
  -DBUILD_WITHOUT_LAPACK=On \
  -DBUILD_RELAPACK=Off \
  .

参考情報

clapack の Android ビルド(ndk-build であるが)

Compile Kaldi for 64-bit Android on Ubuntu 18
https://medium.com/swlh/compile-kaldi-for-64-bit-android-on-ubuntu-18-70967eb3a308

TODO

  • LAPACK をビルドする(clapack を使う)
  • cmake で cross compile を試す.
    • in-source ビルドでなんとかビルド. out-of-tree はちょっとめんどい(既存 config.h を消せばいける)
  • CORTEXA73 での性能を試す(Snapdragon 835 などの A73 アーキでいくらか速くなるはず?)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android/Kotlin + Go で gRPC Bidirectional Streaming するサンプルをゼロから構築

これは何

サーバが Go 、クライアントが Android/Kotlin という構成で 双方からリアムタイムにメッセージを送り合う仕組みとして、gRPC Bidirectional Streaming を使ったサンプルを作るのに必要な設定やコードをまとめたものです。

いろいろ調べながらやっていると新旧どれが正しい情報かわからず結構手間がかかったり、基本的な所でつまづいていることに気づきにくかったりしたため、この記事もしばらくの間しか使えないと思いつつ、記事にまとめておくことにしました。

Go も Android/Kotlin も普段使いしているわけではないため、色々と内容に不備があると思います。何かあれば指摘いただけるとありがたいです!

仕様

  • 文字列ひとつだけのメッセージ Greeting をサーバとクライアント間で双方向で送り合う
  • サーバは数秒ごとに連番付きのメッセージをクライアントに送り続ける
  • サーバはクライアントからメッセージを受信したら標準出力に表示
  • クライアントはボタンをクリックすると TextEdit の内容をサーバに送信する
  • クライアントはサーバからメッセージを受信したら TextView に表示

proto ファイルはこういう内容にしました

proto/sample.proto
syntax = "proto3";

package main;

service Greet {
  rpc Say(stream Greeting) returns (stream Greeting) {}
}

message Greeting {
  string body = 1;
}

サーバ(Golang)

MacOSX Mojave で開発しました。

まずは最新版の protoc バイナリをインストールします

$ brew update     
$ brew upgrade    
$ brew upgrade protobuf 

$ protoc --version
libprotoc 3.6.0

Go 用のプラグインを追加して PATH に追加

$ go get -u github.com/golang/protobuf/proto
$ go get -u github.com/golang/protobuf/protoc-gen-go
PATH=$PATH:$HOME/go/bin

前述の proto ファイル proto/sample.proto をコンパイルして src/sample.pb.go を生成します。今回はサンプルなので main package 内に作ってしまいます

$ protoc --go_out=plugins=grpc:src -Iproto sample.proto

サーバの実装は以下のようにしました。

gRPC のサーバに必要なインターフェースを実装した func (s *myGreetService) Say(stream Greet_SayServer) error が本体です。

そこから 2つの goroutine を起動して無限ループします。それぞれで送信と受信を行いつつ、どちらかでエラーが出たらその接続を終了しています。

src/main.go
package main

import (
    "fmt"
    "io"
    "log"
    "net"
    "time"

    "google.golang.org/grpc"
)

type myGreetService struct{}

func send(stream Greet_SayServer, ch chan error) {
    i := 0
    for {
        message := "message #" + fmt.Sprintf("%d", i)
        log.Println("sending: " + message)
        err := stream.Send(&Greeting{Body: message})
        if err != nil {
            log.Println(err)
            ch <- err
            return
        }
        time.Sleep(1 * time.Second)
        i++
    }
}

func receive(stream Greet_SayServer, ch chan error) {
    for {
        in, err := stream.Recv()
        if err == io.EOF || err != nil {
            log.Println(err)
            ch <- err
            return
        }
        log.Println("received: " + in.GetBody())
    }
}

func (s *myGreetService) Say(stream Greet_SayServer) error {
    log.Println("Stream started.")

    ch := make(chan error)

    go send(stream, ch)
    go receive(stream, ch)
    err := <-ch

    log.Println("Stream closed.")
    return err
}

func main() {
    listenPort, err := net.Listen("tcp", ":5000")
    if err != nil {
        log.Fatalln(err)
    }
    server := grpc.NewServer()
    sampleService := &myGreetService{}
    RegisterGreetServer(server, sampleService)
    server.Serve(listenPort)
}

クライアント(Android/Kotlin)

環境設定

Android Studio 3.5 で環境を作るのに少し苦労しました

まずは Empty Activity のテンプレートから新規プロジェクトを作成。

最初に Gradle のバージョンを最新に変更します。これがないと 後のプラグインが java.lang.NoClassDefFoundError: org/gradle/api/attributes/LibraryElemen を出してしまいハマります

/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip

続いて build.gradle と app/build.gradle に protoc 関連の依存関係設定を追加します。

protobuf-gradle-plugin のドキュメントが一番頼りになりました。 1

なお、各種ライブラリのバージョンは 全て、常に何が最新版なのかを調べてそれを設定 したほうが良さそうです。

/build.gradle
buildscript {
    ext.kotlin_version = '1.3.50'
    repositories {
        ...
        mavenCentral()
    }
    dependencies {
        ...
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
    }
}
/app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' 
apply plugin: 'com.google.protobuf'   // <- これは 順番としてここにないといけない

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.11.0'
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.28.0'
        }
    }

    generateProtoTasks {
        all().each {
            it.builtins {
                java { option 'lite' }
            }
            it.plugins {
                grpc { option 'lite' }
            }
        }
    }
}

dependencies {
    ...
    implementation 'javax.annotation:javax.annotation-api:1.3.2' // 生成されるスタブのためのにこれも必要
    implementation "io.grpc:grpc-okhttp:1.28.0"
    implementation "io.grpc:grpc-protobuf-lite:1.28.0"
    implementation "io.grpc:grpc-stub:1.28.0"
}

クライアントアプリ

proto ファイルは src/java/main/proto/sapmle.proto に配置しました。

Java の パッケージ名を追加しています

...
option java_package = "com.github.reki2000.grpcclient";
...

Activity の具体的な実装内容は以下のようになりました。

MainActivity.kt
package com.github.reki2000.grpcclient

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.util.Log
import io.grpc.ManagedChannelBuilder
import io.grpc.stub.StreamObserver

import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    lateinit private var stream : StreamObserver<Sample.Greeting>
    lateinit private var handler : Handler

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

        handler = Handler()

        Log.i("app","started")
        initGRPC()
        button.setOnClickListener({ onButtonClick() })
    }

    fun initGRPC() {
        try {
            val channel = ManagedChannelBuilder.forAddress("10.0.2.2", 5000).usePlaintext().build()
            val stub = GreetGrpc.newStub(channel)
            val responseObserver = object : StreamObserver<Sample.Greeting> {
                override fun onNext(greet: Sample.Greeting) {
                    onServerMessageReceived(greet)
                }
                override fun onError(t: Throwable) {
                    Log.e("app", "error", t)
                }
                override fun onCompleted() {
                    Log.i("app", "connection closed")
                }
            }
            stream = stub.say(responseObserver)
        } catch (e: Exception) {
            Log.e("app", "grpc connection error",e)
        }
    }

    // サーバからのメッセージを受信したら 画面表示用スレッド経由で textView に表示する
    fun onServerMessageReceived(greet: Sample.Greeting) {
        handler.post({showMessage("Received: ${greet.body}")})
    }

    // ボタンがクリックされたら textView に表示するとともに サーバにメッセージを送信する
    fun onButtonClick() {
        val text = editText.text.toString()

        try {
            val request = Sample.Greeting.newBuilder().setBody(text).build()
            stream.onNext(request)
        } catch (e: Exception) {
            Log.e("app", "grpc error",e)
        }
        showMessage("Sent: ${text}")
    }

    fun showMessage(text: String) {
        textView.text = "${textView.text.toString()}${System.lineSeparator()}${text}"
        Log.i("app", text)
    }
}


なお、アプリの manifest で HTTP を有効にしていないといけません

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.github.reki2000.grpcclient" >
    ...
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
</manifest>

実行

クライアントの画面
image.png

サーバのログ

2020/03/21 13:51:20 Stream started.
2020/03/21 13:51:20 sending: message #0
2020/03/21 13:51:21 sending: message #1
2020/03/21 13:51:22 sending: message #2
2020/03/21 13:51:23 sending: message #3
2020/03/21 13:51:24 sending: message #4
2020/03/21 13:51:25 sending: message #5
2020/03/21 13:51:26 sending: message #6
2020/03/21 13:51:26 received: message
2020/03/21 13:51:27 sending: message #7

だいぶ苦労しましたが、うまく双方向で通信できています!

その他気になっていること

  • エンドポイントが一つだけなので いろいろなメッセージをやり取りする場合も一つのオブジェクトがいろいろな処理のための情報を含むような形にしなければならない
  • TCPが張りっぱなしになるが、通信が切れた・黙ってしまったときの復旧を含めた構成を検討したい
  • この構成でサーバがどれくらいの同時接続に耐えられるか

  1. 注意点として README が長く いろいろな記述サンプルがありますが、Androidの場合は最後の方の "Starting from Protobuf 3.8.0, lite code generation is built into protoc's "java" output. Example:" のところを見なければならないということを理解するまでにだいぶつまづきました 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android/Kotlin + golang で gRPC Bidirectional Streaming するサンプルをゼロから構築

これは何

サーバが golang 、クライアントが Android/Kotlin という構成で 双方からリアムタイムにメッセージを送り合う仕組みとして、gRPC Bidirectional Stream を使ったサンプルを作るのに必要な設定やコードをまとめたものです。

いろいろ調べながらやっていると新旧どれが正しい情報かわからず結構手間がかかったり、基本的な所でつまづくいていることに気づきにくかったりしたため、この記事もしばらくの間しか使えないと思いつつ、記事にまとめておくことにしました。

golang も Android/Kotlin も普段使いしているわけではないため、色々と内容に不備があると思います。何かあれば指摘いただけるとありがたいです!

仕様

  • 文字列ひとつだけのメッセージ Greeting をサーバとクライアント間で双方向で送り合う
  • サーバは数秒ごとに連番付きのメッセージをクライアントに送り続ける
  • サーバはクライアントからメッセージを受信したら標準出力に表示
  • クライアントはボタンをクリックすると TextEdit の内容をサーバに送信する
  • クライアントはサーバからメッセージを受信したら TextView に表示

proto ファイルはこういう内容にしました

proto/sample.proto
syntax = "proto3";

package main;

service Greet {
  rpc Say(stream Greeting) returns (stream Greeting) {}
}

message Greeting {
  string body = 1;
}

サーバ(Golang)

MacOSX Mohave で開発しました。

まずは最新版の protoc バイナリをインストールします

$ brew update     
$ brew upgrade    
$ brew upgrade protobuf 

$ protoc --version
libprotoc 3.6.0

golang 用のプラグインを追加して PATH に追加

$ go get -u github.com/golang/protobuf/proto
$ go get -u github.com/golang/protobuf/protoc-gen-go
PATH=$PATH:$HOME/go/bin

前述の proto ファイル proto/sample.proto をコンパイルして src/sample.pb.go を生成します。今回はサンプルなので main package 内に作ってしまいます

$ protoc --go_out=plugins=grpc:src -Iproto sample.proto

サーバの実装は以下のようにしました。

gRPC のサーバに必要なインターフェースを実装した func (s *myGreetService) Say(stream Greet_SayServer) error が本体です。

そこから 2つの goroutine を起動して無限ループします。それぞれで送信と受信を行いつつ、どちらかでエラーが出たらその接続を終了しています。

src/main.go
package main

import (
    "fmt"
    "io"
    "log"
    "net"
    "time"

    "google.golang.org/grpc"
)

type myGreetService struct{}

func send(stream Greet_SayServer, ch chan error) {
    i := 0
    for {
        message := "message #" + fmt.Sprintf("%d", i)
        log.Println("sending: " + message)
        err := stream.Send(&Greeting{Body: message})
        if err != nil {
            log.Println(err)
            ch <- err
            return
        }
        time.Sleep(1 * time.Second)
        i++
    }
}

func receive(stream Greet_SayServer, ch chan error) {
    for {
        in, err := stream.Recv()
        if err == io.EOF || err != nil {
            log.Println(err)
            ch <- err
            return
        }
        log.Println("received: " + in.GetBody())
    }
}

func (s *myGreetService) Say(stream Greet_SayServer) error {
    log.Println("Stream started.")

    ch := make(chan error)

    go send(stream, ch)
    go receive(stream, ch)
    err := <-ch

    log.Println("Stream closed.")
    return err
}

func main() {
    listenPort, err := net.Listen("tcp", ":5000")
    if err != nil {
        log.Fatalln(err)
    }
    server := grpc.NewServer()
    sampleService := &myGreetService{}
    RegisterGreetServer(server, sampleService)
    server.Serve(listenPort)
}

クライアント(Android/Kotlin)

環境設定

Android Studio 3.5 で環境を作るのに少し苦労しました

まずは Empty Activity のテンプレートから新規プロジェクトを作成。

最初に Gradle のバージョンを最新に変更します。これがないと 後のプラグインが java.lang.NoClassDefFoundError: org/gradle/api/attributes/LibraryElemen を出してしまいハマります

/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip

続いて build.gradle と app/build.gradle に protoc 関連の依存関係設定を追加します。

protobuf-gradle-plugin のドキュメントが一番頼りになりました。 1

なお、各種ライブラリのバージョンは 全て、常に何が最新版なのかを調べてそれを設定 したほうが良さそうです。

/build.gradle
buildscript {
    ext.kotlin_version = '1.3.50'
    repositories {
        ...
        mavenCentral()
    }
    dependencies {
        ...
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
    }
}
/app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' 
apply plugin: 'com.google.protobuf'   // <- これは 順番としてここにないといけない

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.11.0'
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.28.0'
        }
    }

    generateProtoTasks {
        all().each {
            it.builtins {
                java { option 'lite' }
            }
            it.plugins {
                grpc { option 'lite' }
            }
        }
    }
}

dependencies {
    ...
    implementation 'javax.annotation:javax.annotation-api:1.3.2' // 生成されるスタブのためのにこれも必要
    implementation "io.grpc:grpc-okhttp:1.28.0"
    implementation "io.grpc:grpc-protobuf-lite:1.28.0"
    implementation "io.grpc:grpc-stub:1.28.0"
}

クライアントアプリ

proto ファイルは src/java/main/proto/sapmle.proto に配置しました。

Java の パッケージ名を追加しています

...
option java_package = "com.github.reki2000.grpcclient";
...

Activity の具体的な実装内容は以下のようになりました。

MainActivity.kt
package com.github.reki2000.grpcclient

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.util.Log
import io.grpc.ManagedChannelBuilder
import io.grpc.stub.StreamObserver

import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    lateinit private var stream : StreamObserver<Sample.Greeting>
    lateinit private var handler : Handler

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

        handler = Handler()

        Log.i("app","started")
        initGRPC()
        button.setOnClickListener({ onButtonClick() })
    }

    fun initGRPC() {
        try {
            val channel = ManagedChannelBuilder.forAddress("10.0.2.2", 5000).usePlaintext().build()
            val stub = GreetGrpc.newStub(channel)
            val responseObserver = object : StreamObserver<Sample.Greeting> {
                override fun onNext(greet: Sample.Greeting) {
                    onServerMessageReceived(greet)
                }
                override fun onError(t: Throwable) {
                    Log.e("app", "error", t)
                }
                override fun onCompleted() {
                    Log.i("app", "connection closed")
                }
            }
            stream = stub.say(responseObserver)
        } catch (e: Exception) {
            Log.e("app", "grpc connection error",e)
        }
    }

    // サーバからのメッセージを受信したら 画面表示用スレッド経由で textView に表示する
    fun onServerMessageReceived(greet: Sample.Greeting) {
        handler.post({showMessage("Received: ${greet.body}")})
    }

    // ボタンがクリックされたら textView に表示するとともに サーバにメッセージを送信する
    fun onButtonClick() {
        val text = editText.text.toString()

        try {
            val request = Sample.Greeting.newBuilder().setBody(text).build()
            stream.onNext(request)
        } catch (e: Exception) {
            Log.e("app", "grpc error",e)
        }
        showMessage("Sent: ${text}")
    }

    fun showMessage(text: String) {
        textView.text = "${textView.text.toString()}${System.lineSeparator()}${text}"
        Log.i("app", text)
    }
}


なお、アプリの manifest で HTTP を有効にしていないといけません

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.github.reki2000.grpcclient" >
    ...
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
</manifest>

実行

クライアントの画面
image.png

サーバのログ

2020/03/21 13:51:20 Stream started.
2020/03/21 13:51:20 sending: message #0
2020/03/21 13:51:21 sending: message #1
2020/03/21 13:51:22 sending: message #2
2020/03/21 13:51:23 sending: message #3
2020/03/21 13:51:24 sending: message #4
2020/03/21 13:51:25 sending: message #5
2020/03/21 13:51:26 sending: message #6
2020/03/21 13:51:26 received: message
2020/03/21 13:51:27 sending: message #7

だいぶ苦労しましたが、うまく双方向で通信できています!

その他気になっていること

  • エンドポイントが一つだけなので いろいろなメッセージをやり取りする場合も一つのオブジェクトがいろいろな処理のための情報を含むような形にしなければならない
  • TCPが張りっぱなしになるが、通信が切れた・黙ってしまったときの復旧を含めた構成を検討したい
  • この構成でサーバがどれくらいの同時接続に耐えられるか

  1. 注意点として README が長く いろいろな記述サンプルがありますが、Androidの場合は最後の方の "Starting from Protobuf 3.8.0, lite code generation is built into protoc's "java" output. Example:" のところを見なければならないということを理解するまでにだいぶつまづきました 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

簡単なゲームを作ってみよう(Win or Lose)

簡単なゲームを作ってみよう

ドットインストールはじめてのJavaScriptで「#11 簡単なゲームを作ってみよう」をやっあと、
ふと思い立ったのでFlutterで作ってみました。

仕様

  1. 5つのマスがあり、5つのうちランダムで1つが「Win」、4つが「Lose」になる。
  2. 「Lose」が選択された場合、マスに"Lose!"が表示される。
  3. "Lose!"が表示されるのと同時にマスが小さくなる。
  4. 「Win」が選択された場合、マスに"Win!"が表示される。
  5. "Win!"が表示されるのと同時にマスが青からピンクに変わり、回転しながら四角形から円に変わる。

こんなところでしょうか。

作ってみる

とりあえず、以下がマスを操作するソースコードです。
もっと効率の良い書き方や操作の仕方、お行儀の良い書き方など教えていただければありがたいです。

  • winnerはランダムで「Win」を設定しています。
  • for文でマスをつくり、リストのindexとwinnerとが一致したら「Win」の操作、それ以外は「Lose」の操作をします。
  • RotationTransitionでマスの回転、AnimatedPaddingとAnimatedContainerでマスのサイズ、色の変更、文字の挿入をしています。
class MyCard extends StatefulWidget{
  MyCard({Key key, this.index}) : super(key: key);

  final int win = winner;
  final int index;

  @override
  _MyCardState createState() => _MyCardState();
}

class _MyCardState extends State<MyCard> with SingleTickerProviderStateMixin {
  Color _mycolor = Colors.blue[500];
  String _mytxt = '';
  double _width = 100, _heigth = 100, _padding = 10, _radius = 0;

  final Tween<double> turnsTween = Tween<double>(
    begin: 1,
    end: 2,
  );
  AnimationController _controller;

  initState() {
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    );
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return card();
  }

  void _winorlose(){
    setState(() {
      if (widget.index == widget.win) {
        _mycolor = Colors.pink[100];
        _mytxt = 'Win!';
        _radius = 100;
        _controller.forward();
      } else {
        _mytxt = 'Lose!';
        _width = 80;
        _heigth = 80;
        _padding = 20;
      }
    });
  }

  Widget card() {
    return GestureDetector(
      onTap: () {
        _winorlose(); 
      },
      child: RotationTransition(
        turns: turnsTween.animate(_controller),
        child: AnimatedPadding(
          duration: const Duration(milliseconds: 500),
          padding: EdgeInsets.all(_padding),
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 500),
            width: _width,
            height: _heigth,
            child: Center(
              child: Text(_mytxt),
            ),
            decoration: BoxDecoration(
              color: _mycolor,
              borderRadius: BorderRadius.circular(_radius),
            ),
          )
        ),
      ),
    );
  }
}

こんな感じになりました

終わりに

読んでいただきありがとうございました。
そういえば、Google Mapsの操作も放置したままだったな。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む