20191201のAndroidに関する記事は15件です。

AndroidでViewModelのLiveDataをアプリケーションで共有する

概要

AndroidでViewModelに保持したLiveDataの値をアプリケーションで共有したい場合のメモ

方針

本来の仕様では起動中のActivityのインスタンスとViewModelが1対1で対応するため、
Activityのインスタンスが変わってしまったり、異なるActivityのインスタンスからLiveDataをobserveしても、
変更検知ができません

class MyActivity : AppCompatActivity() {
  private val userViewModel: UserViewModel by lazy { ViewModelProviders.of(this).get(MyViewModel::class.java) }
  override fun onCreate(savedInstanceState: Bundle?) {    
    userViewModel.user.observe(this, Observer<User>{ user ->
      // OtherActivityでの変更は検知できない
    })
  }
}

class OtherActivity : AppCompatActivity() {
  private val userViewModel: UserViewModel by lazy { ViewModelProviders.of(this).get(MyViewModel::class.java) }
  override fun onCreate(savedInstanceState: Bundle?) {    
    userViewModel.user.observe(this, Observer<User>{ user ->
      // 変更検知できる
    })
    // データの更新を実行
    userViewModel.fetch()
  }
}

ViewModelProvider.Factory を継承したクラスを作成し、
ViewModelのクラスの名前で ViewModelProviders から取得するViewModelが
一意になるようにすることでアプリケーション内でLievDataを共有できるようにしました。

実装したコード

ViewModelProvider.Factory を継承した ViewModelFactory というクラスを作成

class ViewModelFactory: ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (ViewModel::class.java.isAssignableFrom(modelClass)) {
            val key = modelClass.toString()
            return if(hashMapViewModel.containsKey(key)){
                getViewModel(key) as T
            } else {
                val vm: T?
                try {
                    vm = modelClass.newInstance()
                } catch (e: IllegalAccessException) {
                    throw RuntimeException("Cannot create an instance of $modelClass", e)
                } catch (e: InstantiationException) {
                    throw RuntimeException("Cannot create an instance of $modelClass", e)
                } catch (e: InvocationTargetException) {
                    throw RuntimeException("Cannot create an instance of $modelClass", e)
                }
                addViewModel(key, vm)
                getViewModel(key) as T
            }
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }

    companion object {
        val hashMapViewModel = HashMap<String, ViewModel>()
        fun addViewModel(key: String, viewModel: ViewModel?){
            viewModel?.let {  hashMapViewModel.put(key, it)
            }
        }
        fun getViewModel(key: String): ViewModel? {
            return hashMapViewModel[key]
        }
    }
}

使い方

LiveDataをアプリケーション内で共有したいときのみ、
下記のようにViewModel生成時に ViewModelFactory を渡してあげればOK

class MyActivity : AppCompatActivity() {
  private val viewModelFactory: ViewModelFactory = ViewModelFactory()
  private val userViewModel: UserViewModel by lazy { ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java) }

  override fun onCreate(savedInstanceState: Bundle?) {    
    userViewModel.users.observe(this, Observer<List<User>>{ users ->
      // LiveDataがアプリケーション全体から参照できるようになる
    })
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

THETAにお手軽Linux環境を内包させる

この記事は IoTLT Advent Calendar 2019 の2日目の記事です。

はじめに

リコーの @KA-2 です。

弊社ではRICOH THETAという全周囲360度撮れるカメラを出しています。
RICOH THETA VRICOH THETA Z1は、OSにAndroidを採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます(詳細は本記事の末尾を参照)。

と、定型文をかきつつ、、、
今回の記事はプラグインの仕組みや知識を利用した新しいTHETAカスタマイズ方法についてまとめました。
タイトルをみて「adb shellの話?」と思った方、「遠からず」ですが、もっと自由度がある環境にする話です。

adb shellを上手に利用したAndroidアプリをTHETAにインストールします。そうすると、使い心地はLinux(ssh、apt、git、wgetなども使え、多言語開発可能)。root不要。これまでの使い方と完全共存。切り替え簡単。そんなTHETAコンピューティングが可能になります!
「THETAでこんなことを試してみたい」と思いついてから動くまでが圧倒的に速くなる。THETAがとてもハッカソン向きになる。そんな内容です。

「ロボット着ぐるみの内側にいるペンギンさん」の力を自由に引き出せるようにするのですが、アブナイハナシではありません。安心して頂くため、先に説明します。

今回利用するのはTermuxというアプリです。このアプリをインストールすると、sandbox内(システムが安全に保たれるアプリ固有領域)にお手軽Linux環境が展開されます。あとはCUIでその環境を使いこなすだけです。
Termux開発コミュニティさんは、Google Play だけでなくF-Doroid(ある側面で一番セキュア)にも公式apkを公開しています。このおかげで「THETAを開発者モードにした方はその恩恵を受けられる」というわけです。

このアプリはTHETAにとって、(何ものにもなれるの意図で)万能THETAプラグインと見なせます。THETAを開発者モードにした人のワイルドカードです。

  • もうわかった!という方、自力で始めちゃってください。この記事は困ったときに必要な箇所だけみればOK。
  • ちょっと自信はないけどLinuxということならトライしたいという方、前半戦は丁寧めに書いておきます。各プラットフォーム用のSFTP対応ファイル管理アプリからファイルの読み書きする仕込みはできると思います。興味次第では「コマンドを打てば響くTHETA」を体感してから、コマンドの羅列でシェルスクリプトとか短いPythonあたりのスクリプト言語を体感するのも良さそうです。
  • ラズパイなども含めたLinux系コンピューター経験ありだけど「Androidアプリ開発環境はわからないなぁ」とTHETAいじりを躊躇していた方、劇的に敷居が下がります!THETA自体がラズパイみたいになります。慣れた環境から少しづつAndroidの仕組みに触れ、THETAプラグインやAndroidアプリも作成できるようになれると思います。
  • THETAプラグインきっかけでAndroidアプリ開発環境に触れはじめた方(ワタシはこれ)、THETA固有の仕組みやAndroidへの理解が深まります。なんとなく雛型にしてたTHETA Plug-in SDKの仕組みがわかり、THETA plug-inライブラリに頼らなくて諸々できるとことが体感できます。

レッツTHETAコンピューティング!

THETAまわりで事前に準備が必要なこと

THETAプラグイン開発をしていた方々は既にできています。
今回の記事は、そうでない方々の目にとまりそうなので改めて羅列しておきます。

THETAプラグインのしくみ

今読まなくてもOKです。
2018年10月20日に行われた「RICOH THETA プラグイン開発 ワークショップ #1」資料 の18~21ページです。
考え事ができたとき、仕組みを思い出すと自力で解決できると思うので掲載します。
今回に限らず、普通のTHETAプラグイン作りにも役立つ資料です。

資料抜粋.png

ポイントは以下です。

  • 撮影アプリ(com.theta360.receptor)がTHETA固有事項を担っている
  • 本体ボタン操作なども撮影アプリが受け付けたあと、プラグインへIntentを投げている
  • その他のデバイス固有事項(LEDやOLEDなど)も、プラグインから撮影アプリにIntentを投げると操れる
  • THETA Plug-in ライブラリは撮影アプリ-プラグイン間の上記通信をラップしたもの。通信の詳細はこちらのドキュメント「Broadcast Intent」に掲載されています。
  • プラグインからの撮影方法は2通り
    (1) webAPIが全て使える(内部通信のため、IPアドレス127.0.0.1、ポート8080となる)
    (2) 一般AndroidアプリのようなCameraAPIを使う方法で高速連写など特殊な撮影も可能ですが、細かなところまで自身でコードを書かなければなりません。

Termux について

「はじめに」の章にTermux公式Wikiへのリンクが貼ってありますが英語です。日本語で概要を掴みたい方は以下あたりをご参照ください。

いつでもLinuxコマンドが使える!Androidで動くLinux端末「Termux」【Root化不要】

他にも「Termux」で検索すると日本語の記事も沢山ヒットします。QiitaでTermuxタグがついた記事も豊富です。Google Playでは2019年11月時点で500万ダウンロードもされている人気ツールです。
活発な機能拡張が続いているので、GitHubのソースコードやその他のやり取りからも多くのことが読み取れますよ。

Linuxとの違いについては公式Wikiのこちら

余談ですが、今年の夏「Maker Faire Tokyo 2019」にて「Linuxで開発したいなぁ~」「Python動けばなぁ~」のような声を伺えました。そのおかげで今更ながらこのツールに気づけました。探してみるものですね。

Termuxインストール

事前説明ながくてすみません、やっと作業です。
「はじめに」の章に記載済みF-DoroidのTermuxページから最新のapkファイルをダウンロードしてください。クリックする場所が紛らわしいのでご注意を。

ダウンロード説明.png

ファイル名は「com.termux_82.apk」(2019年11月中旬時点)のようになっていると思います。数字はリリースバージョン番号です。大きな数字ほど最新です。
ダウンロードできたら、Android StudioのTerminalまたは Windowsならばコマンドプロンプトから

adb install -r (ファイルまでのパス)\com.termux_82.apk

と入力するとインストールできます。
(以降「adb」ではじまるコマンドは、この方法で実行すると思ってください。その他についてはTermuxへの入力です。)

はい、これであなたのTHETAにLinux環境が入りました。簡単ですね。

これ以降は、動作を確認しながら使い勝手をよくしていきます。

Termux初期セットアップ

THETAとPCをケーブル接続するのではなく無線LAN経由でTHETAにログインして利用できると物事が捗ります。ラズパイはじめLinux系ボードコンピューターでは常套手段ですね。まずはその環境を整えるのがメインの作業です。それに加えて、最初に行ったほうがよさそうな事項を記載しておきます。

THETAプラグインを作れる方々には既知の事項が多数あります。
各人の理解度に併せ、読み飛ばしたり、作業順を適宜入れ替えたりのアレンジはお任せします。

THETAをPCとUSB接続中にwifi利用可能にしておく

THETAプラグインを作りなれている方は既にしてあると思います。
こちらのadbコマンドを打って、事前にTHETAをPCにUSBケーブルで接続している状態でもTHETAのwifiが使える状態にしておいてください。
この設定は、一度実施すると、パラメータをfalseにしたコマンドを打つか、工場出荷状態に戻すまで維持されます。

THETAの無線LANクライアントモード(CLモード)を設定しておく

Termux環境へのインストールが必要なので、THETAを外部ネットワークに接続できるアクセスポイントへ接続しておいてください。
これは通常の使い方で予め仕込んでおきます。このあたりの動画こちらの動画が参考になります。

ご家庭のルーターに繋いでもよいですし、スマートフォンをテザリングの状態にしてTHETAからスマートフォンへ接続するでもよいです(通信料にはご注意を)。
複数の接続先を覚えておけます。

Vysor環境でキーボードが使える状態にする。

THETAをPCとUSBケーブル接続しVysorの画面にTHETA内部が見える状態にしてください。

001_Vysor.JPG

この状態では、Vysorからはキーボード入力をうけつけないので、こちらのadbコマンドを打って、Vysor環境でキーボード入力ができる状態にします。
このコマンドで行っていることは、「THETAプラグインのしくみ」の章に記載がある「撮影アプリ(com.theta360.receptor)」の一時停止です。再開はシャッターボタン長押し、資料に記載のコマンド、完全電源OFF後の電源ONで行えます。

VysorからTermuxを起動する。

Termuxを起動すると以下画像右側のような画面が表示され、ソフトキーボード入力 や PCのキーボード入力ができる状態になっています。

002_Termux起動2.JPG

apt(pkg)使えます

「外部から必要なパッケージをインストールできる」これがadb shellよりもTermux環境を使う大きな利点となります。そのパッケージ管理ソフトがaptです。aptの管理情報を最新の状態にする作業です。

apt update
apt upgrade

と2つのコマンドを実行してください。
[Y or N]を問われたらYを答えればOKです。
何かのインストールを行う前には、その都度実施する癖をつけておくとよいです。

(余談:pkgも使えます。Termux環境のpkgはaptのラッパーで中身は同じものとのことです。)

テキストエディタのインストール

プログラムを書かない人であっても、僅かながら設定ファイル(テキストファイル)の編集作業が必要となります。
Termuxをインストールしただけの状態でviエディタが使えますが、vimやEmacsやnanoもインストールして使えます。(viって何?vimって何?という方は無難にnanoをインストールしておくと良いです。)

例えば、nanoをインストールする場合は

apt install nano

と打てばOKです。
お好みに応じてインストールし、エディター自体の設定ファイルも編集しておくと便利です。(私はvimで~/.vimrcにset numberと記載しておき、行番号を表示させています。nanoの場合は/etc/nanorcにset linenumbersと記載しておけば同じ感じです。)

ストレージへのアクセス権を与える

この時点でTermuxに読み書き権限があるのは、自身のアプリケーション領域だけで窮屈です(THETAでは、他のアプリ領域も含め最大2GBに制限されています)。そこで、撮影画像が保存される領域(adb shellでは/storage/emulated/0/DCIM配下)への読み書き権限を与えます。
いつでもできますが、Vysorの画面が使えるときにやってしまいましょう。

まず、Vysor画面 Settings → Apps → Termux から、Termuxにストレージアクセスのパーミッションを与えます。

003_パーミッション.JPG

続いて、ターミナルから以下のコマンドを打ちます。

termux-setup-storage

すると、読み書き権限がつくだけでなく~/storage配下にシンボリックリンクができます。
「ls -al ./storage」などを打って確認するとよいでしょう。
「~/storage/dicm」が「/storage/emulated/0/DCIM」と等価です。
(余談となりますが、/sdcard/DCIM は /storage/emulated/0/DCIM のシンボリックリンクです)

004_ストレージ設定.JPG

もし、ストレージアクセスのパーミッションを与えずにこのコマンドを打った場合には、ダイアログが表示されるので「ALWAYS」に答えてから、もう一度同じコマンドを打てばOKです。

このブロックの作業は以上。
ファイルアクセスに関するちょっとした注意事項は本記事後半「SFTP関連Tips集」にまとめてあります。
今は次の作業へすすみましょう。

SSHサーバーまわりの整備

TermuxではOpenSSHが使えます。
以下手順でインストールとセットアップをしてください。

  1. OpenSSHのインストール(apt install opensshを打つだけ)
  2. 秘密鍵&公開鍵ペアの生成(THETA初期設定では、外部機器で生成を推奨)
  3. 公開鍵を ~/.ssh/authorized_keysに仕込む(直接コピペ推奨)
  4. ~/.bashrcを作成して1行「sshd」とだけ書いておく
    (.bashrcはシバンなしでも大丈夫みたいです)

1~3については、以下ページなど(他も多数あります)を参考とすると良いです。

ただし、以下の点を補足しておきます(上の記事より先に見てね)。

  • 2.について
    Termux以外で行うほうが判りやすく操作がラクです。「SSHでのログインを試す」の項で紹介するようなSSH対応クライアントアプリで生成できます。 どうしてもこの時点で、THETA内Termuxで生成した鍵を使いたい方は、こちらの応用で生成したファイルをとりだせます。
  • 3.について
    この段階では、外部機器で作成した公開鍵のファイルをTermuxが参照できる領域にコピーすることが困難です。しかし、公開鍵は文字列が1行だけかかれているテキストファイルなので無理にコピーしようとは思わず、Termux上のエディタで直接~/.ssh/authorized_keysに文字列をコピペするのが最速作業手順かと思います。
  • 4.について
    この作業は、Termuxが起動したらSSHサーバーが自動で立ち上がるようにするための設定です。Termux環境はこれができるのがTHETAにとって最大の利点かもしれません。このシェルスクリプトの記述しだいで様々な振る舞いにできます。まさに「万能THETAプラグイン」です。

ここまででUSBケーブル接続した苦行環境はおしまいです。

TermuxへSSHでログインする

ここからは、THETAとPCを接続していたケーブルを抜いて作業します。

クライアントアプリの紹介

THETAにログインする端末毎に、SSHログインできるクライアントアプリを紹介しておきます。
(前述のとおり、これらのアプリでも秘密鍵と公開鍵のペアが作成できます。)

端末のプラットフォーム クライアントアプリ参考情報
Andoid ConnectBotが無難なようです。他もあります。
iOS 私の知識がありませんがこのあたりを参考に。多数あります。
Windows TeraTermが知らない人居ないレベルでしょうか。他も多数あるかと。
Mac terminalでOpenSSHが使えるはずです。他もあるようです。

クライアントアプリの設定

クライアントアプリの設定で注意することは以下です。

設定項目 設定内容や注意事項
IPアドレス APモードなら192.168.1.1
CLモードはルータによります。(表の下で補足します)
ポート番号 8022になります。
(指定する場合、Termux都合で1024番より大きな数値にしてください)
ユーザー名 空白以外なら何を入れてもOK。
Termuxが自動割り当てして変換してしまいます。
前の作業で作成した秘密鍵のファイルを与えてください。

CLモードで接続する場合の補足です。
THETAにケーブル接続してTermuxのターミナルから「ip -4 a」を打って調べる方法もありますが、毎回ケーブル接続するのは不便です。
ケーブル接続せずにTHETAに割り当てられたIPアドレスを知る方法をいくつか挙げておきます。

接続形態 方法の例
個人用のルーターに接続する ルーターの管理画面で確認することができます。(詳細は機種によるので割愛)
テザリング可能なスマートフォンに接続する スマートフォンのWiFi設定確認画面で確認できます。(詳細は機種によるので割愛)
接続形態によらず、THETAにSSHログインする端末側でLinux系のネットワークコマンドが使える場合

(AndroidスマートフォンにTermuxをインストールするのもありです)
さまざまなコマンドで調べる方法があります。たとえば「nmap -sT -p8022 192.168.*.* 」で8022のポートを開いているTCPプロトコルのIPアドレスを探索するなどです。他arpコマンド利用などもあると思います。

Termuxを起動プラグインにする

通常のTHETAプラグインと同じ操作でTermuxを起動できるようにします。
以下動画の「setup」の所とやり方は同じです。(実は、この動画のプラグイン一覧が表示されるところに「Termux」がみえてるんですよ!探してみてください。)
PC用基本アプリだけでなく、スマートフォンの基本アプリからも設定できます。

THETA Z1をお使いの方は、起動プラグインをTermuxにするのではなく、この動画の Plug-in Launcher for Z1を仕込んでおくほうが便利です。

SSH接続する

準備が整ったらSSH接続してみましょう。
Android機からConnectBotで接続する例を以下に示します。

ログイン.gif

丁寧に手順や仕込みを書いてきたので面倒そうにみえますが、簡単でしょ?

Termuxの終了

Termuxを起動したままSSHをログアウトする方法について説明は不要だと思います。(アプリにより異なります。ターミナルからexitを打つなり、GUIで切断の操作をするなり様々ですが、いずれも簡単です。)

しかし、TermuxはTHETA本体のボタン操作を受け取れません。
このため、Termuxを終了させるためにSSHログインした状態で以下コマンドを打つ必要があります。

am broadcast -a com.theta360.plugin.ACTION_FINISH_PLUGIN --es packageName com.termux

こちらのドキュメント末尾付近「Notifying Completion of Plug-in」に記載があります。

コマンド打つのが面倒という方の強制終了方法として、「電源ボタン短押し」でTHETAをスリープさせることでTermuxを終了させ、すぐスリープから復帰という荒業もあります。

工夫なしで、こんなにTHETAを操れる

外部機器(スマートフォンやPC)からTHETAにSSHログインしてTHETAを操れる環境が整いました。
コマンドラインでできることは必ずプログラムにできます。コマンドラインから少しづつTHETAを操って、振る舞いを確認してみましょう。

この章で紹介することは「プログラミングなし」です。
プログラミングなしでこんなに遊べます。

いきなりLチカ

THETA Vをお持ちの方限定、みんな大好きLチカが簡単にできます。
まずは以下のコマンドを打ってみましょう。

am broadcast -a com.theta360.plugin.ACTION_LED_BLINK -e target LED3 -e color yellow --ei period 500

はい、無線LANの状態を示すLEDが500ms間隔で黄色くチカチカしましたね。

消す場合は以下です。

am broadcast -a com.theta360.plugin.ACTION_LED_HIDE -e target LED3

黄色に点灯させたままは以下です。

am broadcast -a com.theta360.plugin.ACTION_LED_SHOW -e target LED3 -e color yellow 

こちらのドキュメント「Control the LEDs」のブロック を参考にして他のLEDもチカチカさせてみましょう。

  • LED3~8が操れます。
  • LED4~LED8は、カラーLEDでないので色表示ができない(指定不要)
  • 点滅時間の最小値は500[ms] 最大値は 2000[ms]

というくらいが注意点です。

いきなりOLED表示

THETA Z1をお持ちの方、自由に操れるLEDが無いからLチカできないと嘆かないでください。
もっとよい表示デバイスがついていますね。そうOLEDです。
これもコマンド1行で操れるのです。まずは以下を

am broadcast -a com.theta360.plugin.ACTION_OLED_TEXT_SHOW -e text-middle 'This is text-middle String'
am broadcast -a com.theta360.plugin.ACTION_OLED_TEXT_SHOW -e text-bottom 'This is text-bottom String'

消したい場合は以下です。

am broadcast -a com.theta360.plugin.ACTION_OLED_HIDE

こちらのドキュメント「Control the OLED」のブロック が参考になります。

現時点では画像表示に関する以下インテントについて探り中です。
“com.theta360.plugin.ACTION_OLED_IMAGE_SHOW”
“com.theta360.plugin.ACTION_OLED_IMAGE_BLINK”
amコマンドで画像データを渡す方法がまだみつかっていません。。。

OLED表示ちょっと応用

表示器があるTHETA Z1独自の小技となります。
THETA Z1をCLモードにしてTermuxを利用する際、Termuxを起動したらSSHログインするためのIPアドレスがOLEDに表示されていると便利です。

まずはコマンドラインから以下を打ってみてください。

ip -4 a

こんな表示がされると思います。(以下はAPモードの時の例)

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group def ault
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
6: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    inet 192.168.1.1/24 brd 192.168.1.255 scope global wlan0
       valid_lft forever preferred_lft forever

この表示結果を grepコマンドを併用して必要部分のみ抜き出す例は以下となります。
(もっとスマートな方法があるかもしれませんが・・・取り急ぎ)

ip -4 a | grep inet | grep wlan0 | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(?=\/)'
THETAのWLAN状態 上記コマンドの実行結果
APモード 192.168.1.1
CLモードで接続中 ルータに割り付けられたTHETA Z1のIPアドレス
WLANオフ or
CLモードで未接続
表示なし

が表示されると思います。

このコマンドの結果をOLED表示するように .bashrc を記述しておきましましょう。
すでに記載済みの 「sshd」と併せると以下のような記述になります。
(Termuxの.bashrcは、シバンが無視されるようですが、念のためTermux環境用のシバンを書いておきます。通常のシバンを通す方法はこちらなどが参考になります)

.bashrc
#!/data/data/com.termux/files/usr/bin/sh

# launch sshd
sshd

# display self IP
array=$(ip -4 a | grep inet | grep wlan0 | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(?=\/)')
echo $array
am broadcast -a com.theta360.plugin.ACTION_OLED_TEXT_SHOW -e text-bottom $array

動作させるとこんな感じ。(CLモードの例)

IPアドレス表示.gif

CLモードでtermuxを利用するのがとても便利になったと思います。

THETAにプリセットされた効果音を鳴らす。

以下のコマンドを打ってみてください。

am broadcast -a com.theta360.plugin.ACTION_AUDIO_SHUTTER

撮影音が鳴りましたね。

こちらのドキュメント「Controlling Speakers」のブロック を参考にして他のプリセット音も鳴らしてみましょう。

「ACTION_AUDIO_ほにゃらら」の「ほにゃらら」を変えるだけで色々な音がなります。

いきなり撮影(curl)

Termuxはインストールするだけでcurlコマンドが使えます。
THETA S/SC/V/Z1にcurlコマンドが使えるPCやラズパイなどを接続して撮影していた方、その知識まるごと生かせます。
今回は、THETA内Termux → THETA内部処理(撮影アプリ) への指示となるので、IPアドレスとポート番号を「192.168.1.1:80」から「127.0.0.1:8080」に変えるだけです。以下コマンドを打ってみてください。

curl -v -H "Content-Type: application/json; charset=utf-8" -H "X-XSRF-Protected: 1" -X POST -d "{"name": "camera.takePicture" }" http://127.0.0.1:8080/osc/commands/execute

撮影の前後で「ls ~/storage/dcim」を打って画像ファイルが増えることを確認してみるとよいです。

その他の色々なこともできます。
webAPIの詳細はこちらをご参照ください。

これでTermuxから殆どのことが掌握できていることになります。
(ライブビューの処理もAndroidより作り易いです。MOTIONJPEGのストリームをffmpegにいれてあーたらこーたらを別プロセスで処理させつつ...とか作り易いです。)

Tips:Termuxのamコマンドは何者?

余談となります。
amコマンドは「Activity Manager」と呼ばれ、AndroidのADB Shell独自のコマンドです。普通のLinuxにはありません。これが何者なのか少し確認してみましょう。

Termuxで「which am」と打つと

/data/data/com.termux/files/usr/bin/am

と結果がでます。

ADB Shellでは

/system/bin/am

と表示されます。

それぞれの使い方で別のバイナリが実行されています。
ヘルプを表示させると、Termuxのamコマンドはアプリケーションの実行権でできる範囲に機能が絞られていることがわかります。「force-stop」などは使えないので、ある意味安全とも言えます。

SFTPクライアントからデータの読み書きをする。

curlコマンドで撮影したあと、lsコマンドで画像ファイルができていることを確認できますが、画像を見れない歯がゆさがあります。
SSHログインしている端末で画像データみたいですよね。これもできます。
iOSだけの困りごとになりますが、DNGファイルをスマートフォン用基本アプリでひきとれませんよね。これもできるようになります。
自身が作成したプログラムが扱うデータなどを、THETA内部に書き込みたかったりもしますよね。これもできます。

SFTPクライアントの紹介

各プラットフォームそれぞれ、SFTP対応アプリはたくさんあるので、いくつかだけ紹介しておきます。

端末のプラットフォーム クライアントアプリ参考情報
Andoid AndFTPがおすすめなようです。普段お使いのファイル管理アプリが既に対応しているケースも。
iOS FTPManagerがおすすめみたいです。iOSを使う人に試してもらったらjpg,DNG,mp4のサムネイルまで表示されました。すごい。
Windows WinSCPを使ってみました。SSHログインも同時に行える特徴があります。他多数あるのでお好みで。
Mac GUIでとなるとちょっと私の知識不足。割愛させてください。

Termux OpenSSHのSFTP設定

Termux Wiki 「Remote Access」ページの「Setting up password authentication」のブロックにしたがって作業します。

・設定ファイルの編集
sshdの設定ファイルを以下のように変更してください。
(SFTPについてはパスワード認証になります)

$HOME../usr/etc/ssh/sshd_config(編集前)
SendEng LANG
$HOME../usr/etc/ssh/sshd_config(編集後)
SendEng LANG
PrintMotd no
PasswordAuthentication yes
PubkeyAcceptedKeyTypes +ssh-dss
Subsystem sftp /data/data/com.termux/files/usr/libexec/sftp-server

・パスワードの設定
以下コマンドを打ってパスワードを設定します(指示に従い2回入力)

passwd

上記2つの設定をしたら、sshdを立ち上げなおします。「pkill sshd」などとすると、現在作業中のターミナルの通信が切断されますので、ログアウトしてログインするのが良いでしょう。

SFTPでファイルアクセスする。

あとは、接続するだけでGUIのファイル管理ソフトで読み書き自由にできてしまいます。
大抵の場合、アプリケーション内にパスワードを記憶できるのでクリックするだけです。

AndFTPからアクセスした例は以下

SFTP.gif

ログイン→ファイル取得→THETA基本アプリで表示までしています。

SFTP関連Tips集

みえすぎちゃって困るの

おおむね、Android Studioの「Device File Explorer」と同じディレクトリが見えます。ファイルについてはアプリケーションとして権限があるところまでなので、空のディレクトリが見える場合もあります。

見えすぎても使いませんので、クライアントソフト側で「接続したときに最初に表示するディレクトリ」を設定しておくと便利です。各人の用途によりけりですが、$HOMEや/sdcard/DCIM としておくのが無難です。

USBケーブル接続(PTP/MTPアクセス)との違い

USBケーブル接続時(UVCモード以外)のプロトコルはPTP/MTPで、外部機器からのアクセス制限は以下となっています。

  • Androidのシステムからみた /sdcard/DCIM 配下が見えます。
  • 拡張子が jpg, DNG, mp4 のファイルだけ見えます。
  • 読み取りと削除ができます。書き込み禁止です。

上記に該当するファイルのデータベースは完全電源OFFから電源ONしたときと撮影アプリ(com.theta360.receptor)によってファイル操作されたときに更新されます。
撮影アプリを介さず作成/削除された対象ファイルを、完全電源OFFをせずに、PTP/MTPから最新の状態が見えるようにするには、こちらのドキュメント下方「Updating the Database」に記載されているインテントを使う必要があります。

/sdcard/DCIM/orgdir/newfile.jpg のようなファイルを作成した場合

am broadcast -a com.theta360.plugin.ACTION_DATABASE_UPDATE --esal targets DCIM/orgdir/newfile.jpg

/sdcard/DCIM/orgdir/delfile.jpg のようなファイルを削除した場合

am broadcast -a com.theta360.plugin.ACTION_DATABASE_UPDATE --esal targets DCIM/orgdir/delfile.jpg

「DCIM」始まりで対象ファイルまでのパスを指定します。
ファイルが空のディレクトリの追加や削除に対しては、完全電源OFF→電源ONでないと更新されないようです。
「PTP/MTP接続時、電源OFFせずにTermuxで処理したjpg, DNG, mp4ファイルをみたい」という場合だけの話なので、あまり気にしなくても問題ないかもしれません。

THETA内部のタイムゾーン

一般的なTHETAの使い方には、タイムゾーンの概念がありません(Exifなどの仕様にありません)。
Termuxを使うケースに限らず、開発者モードにして、タイムゾーンの概念があるAndroidシステムを介するプログラミングをするとタイムゾーンが気になることがあります。
今回、SFTPクライアントでTHETA内部のファイルタイムスタンプをみたり、「date」コマンド打ったりすると、THETA内部がタイムゾーンをどのように扱っているか簡単に見えるので、THETAの振る舞いに触れておきます。

THETAは、「AndroidのタイムゾーンをUTCに固定して、時刻部分は各国の時間(基本アプリから届いた時刻あわせコマンドの設定値に従う)」という状態で管理されています。
VysorなどからAndroidのタイムゾーンをUTC以外に変更しても、基本アプリをTHETAに接続すると元の状態に戻されます。
基本的にはこのあたりがシビアになることは行わないと思います。弄らずに使用したほうが無難です。

OpenSSHクライアントも使えます

ここまで、設定することが多めなOpenSSHサーバーについて詳しく書きましたが、クライアント動作もできます。
curlコマンドで撮影したら、別マシンで動作しているSFTPサーバーへファイルを書き込むようなプログラムも作れますのでご安心を。
(わかっているとは思いますが、念のため…)

THETA内部でプログラミング

可能性が無限大に広がってしまう話なので、最小限のポイントに絞ります。

以降では、「インストールが必要な事項はCLモードで外部ネットワーク接続」「そうでない場合はAPモードでもOK」ということについて、いちいち記載しませんので各自でその都度判断してください。

git,wgetも使えます

以下二つはインストールしておいたほうが良いです。
特にgitは、GitHubからベースとなるプロジェクト一式を持ってこれるだけでなく、自身の成果物を作業履歴つきでバックアップしたり、公開したりもできて便利です。

apt install git
apt install wget

マルチ言語開発と実行が可能

他にもいろいろつかえるのですが、取り急ぎC,C++コンパイラとpython3をいれましょう。

C,C++のコンパイラはclangです。apt,pkgなどでインストール作業を行うとビルドも自動でなされる場合があるので、Termux環境に必須といっても過言でないかと。以下コマンドでインストールしてください。

apt install clang

インストール後は gcc,cppを打ってもclangが動作するようclangの別名が設定され、外部から取得したmakefileが大抵動作するようになります。

Python3は以下コマンドでインストールしてください。インストール後はpipも使えます。

apt install python

インストールしたあと、バージョン表示をした結果は以下のとおり。

インストール結果.jpg

pythonでwebUIのサンプル

HTTPサーバーが簡単に立つのでPython事例として挙げておきます。

まず flaskというPythonのライブラリをインストールします。

pip install flask

つづいてお好みのディレクトリに以下のコードを書きます。
ブラウザでアクセスしたとき「Hello World!」と表示されるだけのサンプルです。

hello.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_cloud():
       return 'Hello World!'

app.run(host='0.0.0.0')

保存したら以下でHTTPサーバーを起動します。(パスやファイル名はお好みで)
起動するとコマンドプロンプトが戻ってきません。「Ctrl+c」で終了させるとプロンプトが戻ります。

python hello.py

あとは同一ネットワークにいるマシンのブラウザから以下にアクセスします。

http://localhost:5000/

※localhostは、APモードなら「192.168.1.1」CLモードなら「クライアントアプリの設定」の項に出てきた方法で調べてください。

ポート番号を指定していないので5000番になります。8888番を指定すると、通常THETAプラグインのwebUI事例と同じになります。しかし、Termuxビルド時のassetフォルダに「このアプリにはwebUIがあるよ」を示すxmlファイルを置けていませんので、THETA基本アプリからブラウザを起動する振る舞いができない点はご注意ください(Termuxを使うような方は直接入力のほうが楽かと思いますが念のため)。

cat -nでコード表示 → サーバー起動 → 同一ネットワーク上別マシンから表示
をした動作例は以下です。

HTTPサーバー.gif

他のPython簡単事例を作るためのキーワード

サンプルを作る時間がなかったので、THETAで便利そうなキーワードを少々。

  • PycURL(pycurl)を使うと、webAPIを使ったコードが楽にかけると思います。
  • PyDriveをつかうとGoogle Driveアクセスをするコードが楽にかけると思います。
  • NumPyなど当たり前に動くので、姿勢センサー出力を行列演算するコードも楽にかけると思います。

Termux:APIで機能拡張(CUIで姿勢センサーや音再生)

コマンドラインからAndroid固有の機能を使えるようにするTermux:APIの導入方法を紹介しておきます。
最新の情報はTermux:APIのWikiを参照してください。日本語ではこちらの記事が参考になると思います。

沢山のことが行えますが、現時点でTHETAと相性が良いのは以下と思われます。

コマンド名称 できること
termux-sensor 姿勢センサーの情報、主に加速度(3軸)、角速度(3軸)、地磁気(3軸)取得する
termux-media-player 任意の音楽ファイルを再生する

追加機能apkインストール

AndroidのsharedUserIdという仕組みをつかってTermux本体に追加される機能です。
F-Doroidに公開されているTermux:APIの最新apkを取得してインストールしてください。

インストールの仕方はTermux本体のapkインストールと同じです。

Termux本体と同じ署名がされてなければインストールできません。
Termuxに独自の仕掛けを追加するわかりやすい事例にもなっているので、公開されているソースコードをベースに自身でビルドした場合は、Termux本体とTermux:APIの両方に同じ署名をする必要があります。(Google Playが使えるスマートフォンでTermuxを利用するときに、GooglePlayとF-Droidのapkを混在させることができないのと同じ理由です)

Termux内の追加機能インストール

Termuxのシェルから追加機能を呼び出すためのプログラムが必要となります。
Termuxにログインしたら、コマンドラインから以下を入力してインストールしてください。

apt install termux-api

コマンドラインから姿勢センサーを使う

ヘルプを表示すると、オプションが分かります。

$ termux-sensor -h
Usage: termux-sensor
Get information about types of sensors as well as live data
  -h, help           Show this help
  -a, all            Listen to all sensors (WARNING! may have battery impact)
  -c, cleanup        Perform cleanup (release sensor resources)
  -l, list           Show list of available sensors
  -s, sensors [,,,]  Sensors to listen to (can contain just partial name)
  -d, delay [ms]     Delay time in milliseconds before receiving new sensor update
  -n, limit [num]    Number of times to read sensor(s) (default: continuous) (min: 1)
$

-lオプションを使い、このハードウェアで出来ることを確認すると以下です。
1つのセンサーの生データだけでなく、扱いやすく加工したデータを取得できるようです。
結果はJSON形式で得られます。

$ termux-sensor -l
{
  "sensors": [
    "LSM6DSM Accelerometer",
    "AK09915 Magnetometer",
    "AK09915 Magnetometer Uncalibrated",
    "LSM6DSM Gyroscope",
    "LSM6DSM Gyroscope Uncalibrated",
    "LSM6DSM Accelerometer -Wakeup Secondary",
    "AK09915 Magnetometer -Wakeup Secondary",
    "AK09915 Magnetometer Uncalibrated -Wakeup Secondary",
    "LSM6DSM Gyroscope -Wakeup Secondary",
    "LSM6DSM Gyroscope Uncalibrated -Wakeup Secondary",
    "Gravity",
    "Linear Acceleration",
    "Rotation Vector",
    "Step Detector",
    "Step Counter",
    "Significant Motion Detector",
    "Game Rotation Vector",
    "GeoMagnetic Rotation Vector",
    "Orientation",
    "Tilt Detector",
    "Gravity -Wakeup Secondary",
    "Linear Acceleration -Wakeup Secondary",
    "Rotation Vector -Wakeup Secondary",
    "Step Detector -Wakeup Secondary",
    "Step Counter -Wakeup Secondary",
    "Game Rotation Vector -Wakeup Secondary",
    "GeoMagnetic Rotation Vector -Wakeup Secondary",
    "Orientation -Wakeup Secondary",
    "AMD",
    "RMD",
    "Basic Gestures",
    "Facing",
    "Pedometer",
    "Motion Accel",
    "Coarse Motion Classifier"
  ]
}
$

重力加速度を 10msec間隔で取得する場合は以下を打ち込みます。

termux-sensor -s "Gravity" -d 10

こちらもJSON形式で結果が得られます。

姿勢データ取得.gif

コマンドラインで実行した場合には、「Ctrl+c」で終了できます。

コマンドラインから任意の音楽ファイルを再生する

SFTPで外部からファイル書き込みができるようになっていると思いますので、再生したいmp3ファイルをどこかに置いて以下コマンドを実行してみてください。

termux-media-player play <mp3filename>

自然音の音量が小さいのはスピーカー特性によるもので「これまで通り」です。普通に再生できます。
以下記事の音楽ファイルも普通に再生できました。

その他のこのコマンドの使い方は以下でご確認ください。

termux-media-player help

これで、Termux環境で作成するプログラムにも音再生のバリエーションを加えられますね。

惜しくも使えなかったもの

電話をかける、スケジューラーや電話帳の操作をする、各種ポップアップ表示をするというような系統は、明らかにTHETAに不要なのはご理解いただけると思います。

以下については、THETAで使えると便利そうですが、提供されたままの状態では動きませんでした。

No コマンド名称 普通のAndroid機ならできること
1 termux-microphone-record マイクから録音する
2 termux-camera-photo カメラで撮影する
3 termux-usb usb機器と通信できるようにする
4 termux-tts-speak Text To Speechを動作させる

動作しない理由は以下です。
1~3につてはTermux:APIの仕組みに皆さんが手を加えることで動きそうです。

  • 「termux-microphone-record」について
    こちらの記事にある「モノラル音声指定」をTermux環境で行う方法が今時点みつかっていません。
  • 「termux-camera-photo」について
    こちらのドキュメント「Notifying Camera Device Control」に従い、CameraAPIの実行権をTermuxにしたのですが0 Byteのファイルができるだけでした。未調査ですがTermux:APIはCameraAPI2を利用しているのかもしれません。(webAPIが使えますので、このコマンドが使えなくても問題ないとは思います。)
  • 「termux-usb」について
    termux-usbのWikiに記載されたサンプルプログラム動作まではできたのですが… libusbを使ってUSBシリアル通信などを組むには手間がかかりそうです。あと、パーミッションを毎回与えなければならない振る舞いになりました・・・。Linuxらしさは薄れますが、今は他の方法で実現するのが早そうです。
  • 「termux-tts-speak」について
    picoTTSがプリインストールされていることにお気づきの方がいるかもしれません。しかし、製品動作として利用していないため正常動作するものではありません。英語の発話だけですが、プラグイン作成に役立つと思われ、将来動作させたいと考えてはいますが。。。今はお許しを。

その他の拡張や将来性

「TermuxやTermux:APIに手を加えずに利用する」という範疇では、これまでの記事と比較して以下事項ができていません。

  • 本体ボタン操作(=THETAに直接接続したキーボード含む)を受け取る
  • OLEDに画像データを表示する
  • マイクからの音データ利用
  • CameraAPIを利用した高度な映像利用(例えば4K30fpsのライブストリーミングなど)
  • USB OTGで外部機器とシリアル通信する(手間をかければできますが一応記載)

Termux:APIの章で少し触れましたが、こういったTHETA独自のハードウェア構成に依存する事項はTermuxAPIの仕組みを真似することで対応できます。

Androidアプリケーション側のコードとsocket通信で指示をしたりその結果を受け取ったりします。Linuxで対象のハードを使うときの作法を守れないこともあり、Linuxの世界での流用がしにくくなりますが、Termux環境でできないことはないという状況かと思います。

また、機能拡張も行われており

なんてこともあったり、まだ機能追加リクエストレベルですが

なども進みそうです。
なんだか可能性が無限大すぎて怖いレベルです。

まとめ

Termux環境構築にトライすることで、普通のTHETAプラグインの作り方の知識を広くすばやく吸収できたのではないかと思います。

環境構築を終えると各種スクリプトが使えるのが強力です。
「THETAでこれちょっと試したい」ということに直ぐにとりかかれます。最終的に行いたいことの要素を別々に検証したあと、統合動作させることも簡単です。以前に作った要素の使いまわしもしやすいですし、自身が行ったことがないことでもLinux系(特にラズパイ)で実績があれば手数少なく(ネットワーク関係なら大抵はそのまま)移植できる可能性が高いです。

Linuxの心得があるかたなら、Android Studioを立ち上げ apkをビルドし、THETAにインストールするほどのことでもないことがサクサクと行えるようになります。
たとえば、「ブラケットインターバル撮影」「ちょっとした画像処理」「ファイル転送関連もろもろ」「姿勢検出の検証」etcができます。
雛型スクリプトを仕込んでおいて、撮影地でパラメーターだけ少しいじるなんて使い方もありだと思います。

今回の記事は「Termux導入編」といったところでしょうか。いくつか提示した課題解決ができたり、Termux環境のほうが便利と思われることがあったら、たまにはこの系統の続編も書いてみようとおもいます。
皆さんも是非Termux環境を試してみてください。

RICOH THETAプラグインパートナープログラムについて

THETAプラグインをご存じない方はこちらをご覧ください。
パートナープログラムへの登録方法はこちらにもまとめてあります。
興味を持たれた方はTwitterのフォローとTHETAプラグイン開発コミュニティ(Slack)への参加もよろしくおねがいします。

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

androidアプリ 外部アプリを使ってファイルを転送する

androidアプリについて

  • zipファイルを外部アプリ(mail、Lineとか)使って転送する機能を実装しようとして、ハマったのでメモ
  • 結論としては、Android7から『file://』による指定ができなくなったため、fileproviderでuriを作る必要があった

実装

AndroidManifest.xml
    <application>
    <!-- providerの設定 -->
    <!-- android:grantUriPermissionsをtrueにして外部からのファイルへのアクセスを許可する -->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true"> 
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_provider" />
        </provider>
    </application>
MainActivity.kt
            // zipファイルのパス
            val exportDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
            val zipFilePath = exportDir.path + "/sample.zip"

            val intent = Intent()
            intent.type = "application/zip"
            intent.action = Intent.ACTION_SEND
            intent.putExtra(
                Intent.EXTRA_STREAM, FileProvider.getUriForFile(
                    this, applicationContext.packageName + ".provider"
                    , File(zipFilePath)
                )
            )
            intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
            startActivity(intent)
res/xml/file_provider.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <external-path name="external_storage_directory" path="." />
</resources>

・external-path
 Environment.getExternalStorageDirectory()の下に対象ファイルがあるため、external-pathを設定
・name
 適当に
・path
 Environment.getExternalStorageDirectory()直下を指定するため"."を設定

以下、対象ファイルの場所によって要素が変わるのでまとめとく

・Context.getFilesDir()
ファイルがfiles/アプリの内部ストレージ領域のサブディレクトリ内にある場合
<cache-path name = " name " path = " path " />

・getCacheDir()
ファイルがアプリの内部ストレージ領域のキャッシュサブディレクトリ内にある場合
<cache-path name = " name " path = " path " />

・Environment.getExternalStorageDirectory()
ファイルが外部ストレージ領域内にある場合
<external-files-path name = " name " path = " path " />

・Context.getExternalFilesDir(null)
ファイルがアプリの外部ストレージ領域内にある場合
<external-cache-path name = " name " path = " path " />

・Context.getExternalCacheDir()
ファイルがアプリの外部キャッシュ領域内にある場合
<external-media-path name = " name " path = " path " />

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

Android/iOSアプリのLightBlueでtoioコア キューブを動かしてみる

Android/iOSでBLEを直に使ってtoioコア キューブを動かしてみます

 toioコア キューブはBLE通信で制御できるとてもかわいい二輪ロボットです。
https://toio.github.io/toio-spec/
 本来、toioコア キューブはtoioコンソールとセットで動かすものですが、toioコア キューブ単体で、スマートフォンや、ノートPC、はたまたRaspberry PiのようなシングルボードコンピュータなどのBLE通信で動かすこともできます。
 この記事ではスマートフォンのBLE通信を使ってtoioコア キューブを動かしてみます。

スマートフォンでtoioコア キューブを動かしたい

 一番簡単な方法はweb bluetoothを使ってchromeブラウザからURLを開きtoio.jsを動かす方法です。
toio.jsをブラウザで動かしてみた
 動かすだけならこれでOKですが、もうちょっと生っぽくというかBLE通信のプロトコルを直に使って動かしてみましょう。

BLEの簡単な解説

  • BLE機器ははセントラル、ペリフェラルの2種類あります。
  • セントラルが、PCやスマートフォン、ペリフェラルが今回はtoioコア キューブにあたります。
  • セントラルはペリフェラルの出すアドバタイジングパケットをみて、どういう素性の機器かを判別し、ペリフェラルに接続します。
  • セントラルは接続したペリフェラルから、値を読み/書き/通知することのできるデータフィールドの情報を得ることができます。このデータフィールドのことをキャラクタリスティックといいます。
  • キャラクタリスティックは以下の3つの性質をもちます。
  • 読み(read)は値を読み込むことができるフィールドです。
    • 例えばバッテリー残量、ボタンを押したかどうかなどです。
  • 書き(write)は値を書き込むことでペリフェラルの動作に変化を与えます。
    • 例えばLEDの色の変更、モーターの回転速度の変更などです。
  • 通知(notify)は読み(read)に似ていますが、データフィールドの値に変化があったときにペリフェラルに値を通知します。
    • 例えばボタンを押したかどうか、モーションセンサによる衝突検知、読み取りセンサによるtoioコア キューブの置いてあるマットのIDやシールのIDの値が変化したときにどんどん通知されてきます。
  • toioコア キューブの場合、読み(read)できるキャラクタリスティックは通知(notify)にも対応しています。
    • モーター、設定のようにwrite、read、notifyの全部に対応しているキャラクタリスティックもあります。
  • BLEはほかにもいろいろありますが、とりあえずこれだけ知ってれば遊べます。
  • Android/iOSアプリのLightBlueを使うと簡単にキャラクタリスティックの読み(read)、書き(write)、通知(notify)を試すことができます。
  • Androidアプリ LightBlue
  • iOSアプリ LightBlue

準備するもの

  • スマートフォン(AndroidかiOSのもの)
  • LightBlueアプリをインストールする 前章のURLからそれぞれのOS用のアプリをインストールしてください。
  • toioコア キューブ 1個
  • 必要に応じて「トイオ・コレクション」のマットやステッカー

さっそくやってみよう

以下ではAndroidアプリのLightBlueの画面を使って説明します。iOSアプリ版もほぼ変わらない操作です。

LightBlueアプリを起動します。

  • 起動時にbluetoothへのアクセス許可、あるいは位置情報へのアクセス許可の確認画面が出る場合は許可します。(許可しないとBLE通信ができないのでtoioコア キューブと通信できません)

[Screenshot_20191201-224104.png]
[Screenshot_20191201-224113.png]

  • 以下の画面が出たら、「toio Core Cube」をタップして選びます。 [Screenshot_20191202-080759.png]
  • 「toio Core Cube」の下に出ている16進数はBLE機器ごとに割り当てられているアドレスです。このスクリーンショットでは下半分(3バイト分)を消していますが、実際には6バイト分あります。この6バイトで個体判別することができます。

toioコア キューブと繋がった状態

[Screenshot_20191202-080814.png]

  • toioコア キューブとつながるとこの画面になります。
  • この画面を下の方にスクロールして、以下の赤枠のところを表示します。 Screenshot_20191202-080907.png]
  • これがtoioコア キューブのキャラクタリスティックです。ここをタップするとキャラクタリスティックの選択画面が出ます。
    Screenshot_20191202-080927.png]

  • この中から読み(read)/書き(write)/通知を受ける(notify)したいキャラクタリスティックを選びます。

  • さて、ここでtoio コア キューブ 技術仕様のページの「通信概要」のところを一度みてみましょう。

バッテリー残量を読む

  • 前章のキャラクタリスティック選択画面から「Battery Information」をえらびます。

[Screenshot_20191202-080939.png]

  • バッテリーのキャラクタリスティックについての説明が出ます。赤枠で囲ったところにReadableとあり、チェックマークがありますので、「読み」ができます。その下にはWritableとありますが×になっているので「書き」はできないことを示しています。さらに下のSupports notifications/indicationsにはチェックマークがついているので「読み」に加えて「通知」もできることがわかります。
  • この画面を下のほうにスクロールしてREAD/INDICATED VALUEのところまできたら「READ AGAIN」ボタンをタップします。

[Screenshot_20191202-081011.png]

  • バッテリー残量の値が読み出されて表示されます。16進数で「64」なので100ですね。(「バッテリー」の仕様も確認してみてください)

[Screenshot_20191202-081024.png]

LED(ランプ)を灯もす

  • 今度は「書き」のほうをやってみます。戻るボタンで一旦、キャラクタリスティック選択画面まで戻って今度は「Light Control」を選びます。

[Screenshot_20191202-081050.png]

  • 今度はWritableのみ有効なので「書き」しかできません。
  • この画面を下の方にスクロールしてWRITTEN VALUEのところまできたら、toioコアキューブ仕様書の「ランプ」のところを参照し、「書き込み操作」の「例」のデータ「03100101FF0000」を書き込んでみます。
    • 「例」では0.16秒間LED(ランプ)を赤く点灯するデータが示されています。
  • 16進数で書き込んで「WRITE」ボタンをタップすると、toioコアキューブの底面のLED(ランプ)が0.16秒間赤く光ります。0.16秒は一瞬ですので気をつけてよく見ていてください。 [Screenshot_20191202-081155.png]

ボタンの状態を読む、変化した通知をうける

  • 続いて今度はボタンの情報を読んだり、通知を受け取れるようにしてみます。
  • 戻るボタンで一旦、キャラクタリスティック選択画面まで戻って今度は「Button Information」を選びます。

[Screenshot_20191202-081257.png]

  • この画面を下の方にスクロールして、[READ AGAIN]のボタンをタップすると、タップした瞬間のtoio コア キューブ底面のボタンの状態(押している、押していない)を読み取ることができます。

[Screenshot_20191202-081313.png

  • このスクリーンショットでの値は01 00、toio コア キューブのボタンは押されていません。
  • toioコアキューブのボタンを押した状態で、もう一度[READ AGAIN]のボタンをタップすると、今度は01 80です。値の意味はtoio コア キューブ技術仕様書の「ボタン」のところで調べてみましょう。

[Screenshot_20191202-081327.png]

  • [READ AGAIN]のボタンのとなりの[SUBSCRIBE]ボタンをタップすると、通知(notify)を受けるようになります。値が変化したときに通知されます。
  • この状態でtoio コア キューブのボタンを押したり離したりすると、状態が変化するタイミングで以下のように値が通知されてきます。

Screenshot_20191202-081414.png]

  • 通知をやめるときは[UNSUBSCRIBE]ボタンをタップします。

Screenshot_20191202-081355.png]

マットの上の座標などを通知させる

  • ここまでくればもうわかりますね。戻るボタンで一旦、キャラクタリスティック選択画面まで戻って今度は「ID Information」を選びます。
  • そして画面の下の方までスクロールしてから[SUBSCRIBE]ボタンをタップします。

Screenshot_20191202-081536.png]

  • toio コア キューブをマットに置いてみてください。また、位置を変えたり角度を変えたりしてみてください。ものすごい勢いで位置、角度データが通知されてくるのがわかります。
    • 通知されてきた値の意味はtoio コア キューブ技術仕様の「読み取りセンサー」で確認してみてください。

さいごに

 スマートフォン(Androido/iOS)のLightBlueアプリを使って、toio コア キューブと直接BLE通信して動かしてみました。あ、モーターは今回使ってないので「動いて」はいないですね。:sweat:
 まあ、toioコアキューブ技術仕様の「モーター」をみて、いろいろやってみてください。特に2.1.0になってからモーター制御コマンドでできることが増えていますのでなかなかやりがいがあると思います。
 ここまでわかってしまえばBLEでのtoio コア キューブのコントロールはそんなに難しくありません。Windows、Linux、MacOS、その他いろいろな環境で、お好みのプログラミング言語でtoio コア キューブを動かして楽しみましょう。

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

Android 端末上で開発環境を整えてみた

本記事は、サムザップ Advent Calendar 2019 #2 の初日の記事です。

前書き

普段は SRE として既存タイトルの保守運用・改善を行いつつ、新規プロジェクトへ向けた標準化、構築運用がメインタスクです。他にもエンジニア採用と育成など、幅広く業務を行なっています!

この記事は出張前の6時間(移動時間含む)を使ってどこまで Android端末に開発環境を整備できるかトライした記事になります。(スクリーンショットは後から取得したものです。)

やりたいこと

  • PCを持ち歩くのが辛いので、普段使いのAndroid端末を開発作業環境にする
  • 個人の開発環境は、AWS と python に依存しているので使えるようにする
    • ssh (4G回線なので固定IPが使えないので、SSMで代用できないか検証)
    • python (できれば anyenv 経由の pyenv でインストールしたい)
    • pip で aws-cli を使えるようにする
    • bash / zsh のスクリプトが書けること

やったこと

さあ、残り5時間半で頑張っていきます!

Andoird で Terminal が使えるようにする

2019年12月現在、terminal emulator は数多く Goole PlayStore に登録されてる。
Termux を見つけた。
基本的なシステムコマンド or ssh のみ使えるものが多いが、Termux はaptやpkg を用いてパッケージの追加が可能だ。
https://github.com/termux/termux-packages

インストール完了後の起動画面
Screenshot_20191201-125242452.jpg

ここで、ls や df などのコマンドが使えることを確認した。
かなり便利だ。スマホでの作業環境構築が一気に捗った気がする。

(さぁ、移動しなくては。)

python のインストール

package リストに python があるのは確認済みなので何も考えずに実行してみる。

Termux上で実行
apt install python

pip を含めて使えるようになった。
python-pip.png

同じ要領で以下のパッケージもインストール

Termux上で実行
apt install git
apt install openssh

(ここで残り3時間程度。スクリーンショットの共有ができないくらいに空港のWi-Fiが弱い...)

ここまでで、python, pip, ssh, git のコマンドが実行できることは確認した。

さあ、搭乗時間が迫ってきた。anyenv化は将来的に対応するとして、aws-cli はめちゃめちゃ大事だ。これがなければ、何もできない....

Termux上で実行
pip install awscli

Screenshot_20191201-125629560.jpg

意外にさっくりできてしまった!
(搭乗手続きのため、中断。)
(気がつけば残り2時間程度。)

Termux上で実行
aws s3 ls
2019-02-01 18:20:37 **********************************
2019-01-01 20:13:29 **********************************
2018-12-01 16:31:29 **********************************
2018-11-01 16:36:22 **********************************
2018-10-01 11:06:09 **********************************

実行確認をした。(モザイクが面倒だったので、テキストをコピペしました。)

ssm が使えるようにセッティング

Termux実行
mkdir ~/.ssh
touch ~/.ssh/config
vim ~/.ssh/config

ここで気が付いた... vimが標準インストールされていない...

Termux実行
apt install vim

(残り1時間20分)

Termux実行
vim ~/.ssh/config

Host i-* mi-*
    ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"

これはインスタンス名を指定した時に、自動で ssm を経由してポート解放せずログインできるようにするおまじない。ただし、対象のEC2にはインスタンスロールなどで、ssmによるログインを許可しておく必要がある。
その辺りは、AWS Systems Manager のセットアップを参照してくだされ。

aws コマンドが使えるように credential をセット

Termux実行
vim ~/.bashrc

AWS_SECRET_ACCESS_KEY=****************************************
AWS_DEFAULT_REGION=ap-northeast-1
AWS_ACCESS_KEY_ID=********************
Termux上で実行
ssh leosuke@i-**************

result.png

セッションマネージャのプラグインがインストールされていない!
ということでインストールしようとしたが... windows用のexe, amd用のrpm と deb しかなく....
alienコマンドで変換してファイル共有してみたが... イントールできなかった。

さらに aws help を利用しようとしたら、groff がなく... groffもインストールできなかった。
(ここで時間が終了。作業時間は3時間弱くらい。)

まとめ

Android 端末で実現できなかったことがあるものの、割と使える環境が整った!
(記事に書いていない部分の後悔としては、bluetooth キーボード。トラックパッドがないものを買ってしまったのがよくなかった)

改善したい点は、termux で aws session-manager-plugin と groff がインストールできなかった。Groffはアウトプットフォーマッタだが、こんなに影響範囲があるとは思わなかった。session-manager-plugin の rpm や deb ファイルのアーキテクチャを amd64 から aarch64 に変換する方法も検討したが、無理だった。

ひと昔前なら root 化しなくてはできなかったことが、端末スペックの向上とアプリケーションによって割と簡単に導入できるようになった。少ない時間でゴリゴリと進められて良かったので、Termux の disabled-packages を使えるように コミットしていこうと思った。

明日は @hiroki_shimada さんの記事です。
ーーーーーー

後日談

mac の bundle用 Python スクリプトでインストールしてみた。
kex_exchange_identification というエラーが出て結局 ssm 経由では接続できなかった。

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

最近Flutterで作ってるもの

Flutter #2 Advent Calendar 2019の2日目の記事です。

今までFlutter、Dart関連はちょくちょく触ってましたが、ちゃんとアプリとして公開等したことがなく、Flutter Web対応も発表されたので、もうちょっと深く触っていきました。

自分のレベル感

  • 普段業務: Webプログラマ(Rails) 5年程度
  • iOSとAndroidも一度は開発の経験有り

作ってるアプリ概要

  • Bouyomisan Flutter
    • 以前 Dart で作ったサービスのリプレイス
    • Flutter web
  • Regonn&Curry.fm
    • ポッドキャストを聞くためのスマホアプリ

CIやデプロイはCodemagicを利用しているので、そのあたりも所感に含めて書いていきます。

Bouyomisan Flutter

以前、YouTubeLiveコメント読み上げさんというWebサイトを、Dart1.0時代に作ってました。
YouTubeLiveのコメントを読み上げてくれるツールをDartで書いてみた
YouTubeLiveコメント読み上げさん
しかし、一度作って放置していたら、Dart1.0の環境構築が難しくなりメンテナンスが出来ない状態になっていました。
Dartでのコードが残っているので、そのままFlutterでできないかリプレイスに挑戦してみました。

Bouyomisan Flutter
bouyomisan_flutter.png

所感

dart:html が使えて、ブラウザの文章読み上げ機能が使えた。

リプレイス前がDartのアプリで、更に文章をブラウザで読み上げるために Web Speech API の SpeechSynthesisUtteranceを利用していたので、HTMLで記述したい部分がありました。Flutterだと、Flutter UIで画面を構築して、web版も全体を覆う感じにスタイルがなっているので、HTMLとかを扱うのが難しいかなと思っていたんですが、 dart:html が呼べたので、

    var u = new SpeechSynthesisUtterance();
    u.text = text;
    u.lang = 'ja-JP';
    u.pitch = _pitchValue;
    u.rate = _rateValue;
    u.volume = _volumeValue;
    window.speechSynthesis.speak(u);

みたいにすることで、実際にブラウザで読み上げることができました。
ただし、 dart:html がimportされていると、スマホアプリのビルドは失敗するみたいです。

Adsenseはまだ使えなさそう

Flutterだったら、もし広告を配置する場合はAdmobとかを使うと思いますが、web版だったらAdsenseを貼りたいですよね。
けど、まだ、ここらへんは対応していないみたいで、FlutterのIssueになっています。

Support Google AdSense in Flutter Web applications · Issue #40376 · flutter/flutter(ただ、Issueの中身をみると、なぜかコードがnendのものだったりして、どれだけAdsense対応に前向きなのかはわかりません)

Codemagicを利用したら、webサイトはすぐ公開できるので良かった

DartでのサービスのときはAWS S3にファイル置いてありましたが、Codemagicには、codemagic専用の公開URLも用意されているので、CIが通ったら公開されるようにしてありました。

Webは最初のアクセスで読み込むためラグが発生する

実際にWebアプリにアクセスしてもらったら、わかりますが、アクセス時にはFlutterのロード時間が1~2秒発生してしまいます。
何も設定しないと真っ白な画面が出てしまうので、ユーザ的には一瞬固まったと感じてしまいそうなので、現在は「読み込み中」と書き込んでありますが、文字を出すと今度は、Flutterが読み込まれる前と後でスタイルも変わってしまうので、本番で利用するにはどうしたらいいのかなと悩んでます。

Regonn&Curry.fm

普段は、データサイエンス系の話題を取り上げている、Podcastをやっています。
専用のPodcastを聞くアプリとして、将来的にはアプリ限定のコンテンツ等も出せたりすると面白いのかなと思ってます。
まだ現状は聞くだけしかできませんが、とりあえずAndroid版の公開までできました。

Regonn&Curry.fm - Google Play のアプリ

利用しているライブラリ

所感

結局リリース周りのネイティブアプリ知識は必要

iOSとAndroidもネイティブアプリも開発とリリース経験はあったので、codemagicを使ってデプロイ等をしていますが、結局iOSやAndroidアプリとしてリリースする際には、証明書関係が必要で、そこらへんの知識がないと厳しいなと思いました。

Windows開発機だと、少し辛い

私は普段、VRコンテンツとかデータサイエンス関連でGPUを使う機会も多いので、Windows環境がメインです。
FlutterだとiOSアプリも作れますが、結局podをインストールしたり、開発版ビルドはMacでするため、いちいちMac環境での作業が入るのは少し面倒(Mac使いだったら、Androidもビルドできるので良さそう)。
Codemagicを利用すれば、リリースビルドの作成とかはクラウド上でできて、そのままiTunes Connectにアップロードもできるので楽だった。

結論

  • Codemagicで、Webとスマホアプリのリリースビルドとデプロイまでできるのが素晴らしい
  • Webはもうちょっと、開発が進むのを待ちたい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

人生で初めて意図的にtry catchを使った話

こんにちは、ヨースケです。
qiita1.PNG
記事を出して3つ目の僕がAndroidアプリ開発をする上で気を付けていることが300以上のviewsにいいねが4も付きました!ありがとうございます!励みになりますなります。m(v _ v)m

  • 以前は..
  • 初めてのアプリで

以前は..

 意図的じゃなかったらどうやってtry catchしてんだwって話なんですが、今までは参考書とかに書いてある通りコードを打って「あぁ、例外処理なんだな」と思って何にも気にしていませんでした。なので、try catchをする意味も理解できていませんでした。食わず嫌いも少しあったかもしれません。

初めてのアプリで

qiita1-1.png

 初めて作ったアプリで2進数、16進数変換をしたいときに入力された数字が整数じゃないと計算できない、そもそも整数以外で変換ボタンを押すとNumberFormatExceptionのエラーが発生してしまいます。何とかそのエラーを防ぎたいと目につけたのが今まで避けていたtry catchです。(一部抜粋です↓)

MainActivity.java
 //int型以外の数値が入ったら2進数変換ができないのでエラー処理
                    try{
                        int error = Integer.parseInt(calc_text.getText().toString());
                    }catch (NumberFormatException e){
                        calc_text.setTextColor(Color.RED);
                        Toast.makeText(MainActivity.this,"変換は整数でしか行えません!または範囲外の数値です",Toast.LENGTH_SHORT).show();
                        break;
                    }

これは入力された数字がInt型以外、つまりNumberFormatExceptionのエラーが発生するなら文字を赤くしてトーストでエラー内容を表示するといった内容です。(switch内なのでbreakしてます)
 何て便利なんだ!とそのときは感動を覚え、ちょっとは成長できたかなーと思ったものですが、やっぱり何でもやってみないと分からないものですね(当たり前)。

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

AndroidでListViewと関数オブジェクトを使ったメニュー画面を作る。

はじめに

以下のようなListViewを使ったメニュー画面を作るときに、
項目ごとに実行される処理が異なるので、
その処理部分を関数オブジェクト化してみたら、
一元管理できて便利だったのでまとめておきます。

レポジトリ

https://github.com/cnaos/ListMenuExample

利用技術

MainActivity

MainActivity.kt
class MainActivity : AppCompatActivity() {

    // wada811さんのDataBinding-ktxを使っています。
    // DataBinding-ktxを使うとonCreaete内のbindingの初期化コードが不要になります。
    //
    // https://github.com/wada811/DataBinding-ktx
    //
    // private lateinit var binding: ActivityMainBinding

    private val binding by viewBinding { ActivityMainBinding.inflate(it) }

    companion object {
        // メニュー画面のリストに表示するラベルと
        // タップされたときに実行する処理の定義
        val listItems = listOf(
            MyMenuItem("Toast表示") { activity->
                Toast.makeText(activity, "Toast Test", Toast.LENGTH_LONG).show()
            },

            MyMenuItem("何もしない"),

            MyMenuItem("アプリ終了") { activity->
                activity.finish()
            }
        )
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

//        binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
//        binding.lifecycleOwner = this

        val adapter = MyMenuListAdapter(this, listItems)
        binding.listView.adapter = adapter

        // リスト項目がタップされたときの処理
        binding.listView.setOnItemClickListener { parent, view, position, id ->
            val menuItem = listItems[position]

            // menuItemにセットされている関数オブジェクトを実行する
            menuItem.executeBlock?.invoke(this)
        }
    }
}

Databindingの初期化にwada811さんのDataBinding-ktxを使っている以外は特別な事はしてません。

やってる事は以下の3つです。

  1. ListView用のカスタムArrayAdapter(MyMenuListAdapter)を作る
  2. ListViewにカスタムArrayAdapterをセットする
  3. ListViewの項目がタップされた処理を設定する

メニュー画面に表示するリストの定義部分

MainActivity_listItems
    companion object {
        // メニュー画面のリストに表示するラベルと
        // タップされたときに実行する処理の定義
        val listItems = listOf(
            MyMenuItem("Toast表示") { activity->
                Toast.makeText(activity, "Toast Test", Toast.LENGTH_LONG).show()
            },

            MyMenuItem("何もしない"),

            MyMenuItem("アプリ終了") { activity->
                activity.finish()
            }
        )
    }

companion objectに定義しているlistItemsがメニュー画面に表示するリストの定義です。
画面に表示されるラベルと、項目をタップした時に実行される処理をラムダで記述してあります。

MyMenuItem

MyMenuItem.kt
class MyMenuItem(
    // リスト項目に表示されるラベル
    val label: String,

    // リスト項目をタップしたときに実行される処理
    val executeBlock: ((activity: Activity) -> Unit?)? = null
)

コンストラクタ引数executeBlockの型は関数オブジェクトで、
その関数オブジェクトの引数はActivityで、
その関数オブジェクトの戻り値はUnitです。

ListViewのタップ処理

MainActivity_onCreate
        // リスト項目がタップされたときの処理
        binding.listView.setOnItemClickListener { parent, view, position, id ->
            val menuItem = listItems[position]

            // menuItemにセットされている関数オブジェクトを実行する
            menuItem.executeBlock?.invoke(this)
        }

ここが核心部分です。

ListViewの項目がタップされると、このリスナが呼び出されるので、
positionから対応するMyMenuItemを取り出して、
MyMenuItemのインスタンスに定義されている関数オブジェクトを実行しています。
関数オブジェクトの引数thisにはactivityを渡しています。

MyMenuListAdapter

MyMenuListAdapter
class MyMenuListAdapter(
    context: Context,
    menuList: List<MyMenuItem>,
    private val onClickListener: ((View, MyMenuItem) -> Unit)? = null
) : ArrayAdapter<MyMenuItem>(context, 0, menuList) {

    private val inflater = LayoutInflater.from(context)

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val binding =
            if (convertView == null) {
                val tmpBinding: MenuListItemBinding = DataBindingUtil.inflate(
                    inflater,
                    R.layout.menu_list_item,
                    parent,
                    false
                )
                tmpBinding.root.tag = tmpBinding
                tmpBinding
            } else {
                convertView.tag as MenuListItemBinding
            }

        binding.menuItem = getItem(position)

        return binding.root
    }
}

ListView用のDataBindingを使ったArrayAdapterです。
ここは特別なことはやってません。

画面のlayout定義とかその他

activity_main.xml(layout)

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <ListView
            android:id="@+id/list_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

ListViewが1つあるだけの画面です。

menu_list_item.xml(layout)

menu_list_item
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="menuItem"
            type="io.github.cnaos.example.listmenuexample.MyMenuItem" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/menu_label"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="16dp"
            android:text="@{menuItem.label}"
            android:textSize="18sp" />
    </LinearLayout>
</layout>

ListViewの各項目の表示に使われるlayoutの定義です。

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

Jetpack Composeは速い?遅い?

この記事は Android Advent Calendar 201910日目の記事です。
これまでの有益な記事たちと違って、非常に駄文めいているので気休めでご覧ください。

Android Studio 4.0でJetpack Composeが使用可能になりました。
Jetpack Composeを使用することでUIをKotlin上から宣言的に記載することができます。

さて、Jetpack Composeといえば思い出されるのがAnkoで、これもKotlin上から宣言的にUIを記載するもの
そのときによく言われていたのが ANKOはXMLをパースしないので性能面で有利ということ。

ではJetpack Composeではどうなのでしょう。

ここでは以下の環境でJetpack Composeと従来的なxmlを使ったViewの速度比較をしてみます。

環境は次の通り

Android Studio 4.0 Canary 4
Kotlin 1.3.60-eap-25
androidx.ui:ui-layout:0.1.0-dev02

単純にTextViewを10件並べたものを用意してみます。
アプリの測定時間はLogcatの Displayed の時間をもとに測定。
実行環境はAPI28のエミュレータで、MacBookPro 13インチ上で実行しています。

計測は10回繰り返し、その平均と最遅、最速を取得しています。

Jetpack Compose

Activity上でTextを10個並べます

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {

                Column {
                    Text(text = "Hello Android!")
                    Text(text = "Hello Android!")
                    Text(text = "Hello Android!")
                    Text(text = "Hello Android!")
                    Text(text = "Hello Android!")
                    Text(text = "Hello Android!")
                    Text(text = "Hello Android!")
                    Text(text = "Hello Android!")
                    Text(text = "Hello Android!")
                    Text(text = "Hello Android!")
                }
        }
    }

結果

単位 ms
864
958
896
848
932
929
879
895
1255
828
最遅 1255
平均 928.4
最速 828

Linear Layout

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

    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" />
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" />
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" />
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" />
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" />
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" />
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" />
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" />
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" />
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" />
</LinearLayout>

結果

単位 ms
497
610
590
576
643
639
684
482
594
500
最遅 684
平均 581.5
最速 482

Constraint Layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView android:id="@+id/text1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" />
    <TextView android:id="@+id/text2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/text1" />
    <TextView android:id="@+id/text3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/text2" />
    <TextView android:id="@+id/text4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/text3" />
    <TextView android:id="@+id/text5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/text4" />
    <TextView android:id="@+id/text6" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/text5" />
    <TextView android:id="@+id/text7" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/text6" />
    <TextView android:id="@+id/text8" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/text7" />
    <TextView android:id="@+id/text9" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/text8" />
    <TextView android:id="@+id/text10" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Android!" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/text9" />

</androidx.constraintlayout.widget.ConstraintLayout>

結果

597
555
604
592
576
549
567
666
551
652
最遅 666
平均 590.9
最速 549

比較

結果を比較してみます。

測定結果 Jetpack Compose Linear Layout Constraint Layout
最遅 1255 684 666
平均 928.4 581.5 590.9
最速 828 482 594

予想に反して、Jetpack Composeが一番遅いという結果になってしまいました。
最速の場合でさえ、Linear LayoutやConstraint Layoutの最遅より遅いです。

Linear Layoutはさすがの最速を決めました。
ただし、このレイアウトはLinear Layoutに特化したレイアウトであることに注意が必要で、Linear Layoutはその性質上、複雑なレイアウトを作れないというのには注意が必要です。

その点、意外な好成績だったのがConstraint LayoutでLinear Layoutとほとんど性能差がないという結果に

Jetpack Composeはまだdev02でパフォーマンス改善に手がつけられていないという可能性はあり今後この数値は逆転するかもしれませんが、現状では性能を考えるとシンプルなレイアウトはFramelayoutやLinearLayoutを使い、複雑なページはConstraint Layoutを使うのが良さそう。
性能面を理由にJetpack Composeを使う理由はなさそうです。

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

[Unity] Androidアプリの64ビット対応でつまづいた話

はじめに

Play StoreにAndroidアプリをあげる際に64ビットにしてね!!と言われ,対応したはずなのに64ビット対応できてないと何度も怒られようやく解消できたので,解決方法をまとめます.

なんで64ビット対応するの?

参考
ざっくりいうと,2021年8月以降64ビットしか扱えないから対応してねという理由.

環境

  • Mac OS Mojave 10.14.6
  • Unity 2018.3.6f1

調べたとおり64ビット対応する

このサイトなどを参考に64ビット対応します.
1. Unity>Preference>External toolsのNDK部分を埋める.
スクリーンショット 2019-11-22 16.51.13.png

  1. Unity>Edit>Project Settings>Player>Configurationで,Script BackendをIL2CPPにして,ARM64にチェックマークを入れる.

スクリーンショット 2019-11-22 16.51.51.png

これでビルドすれば,64ビット対応完了!!!のはずだった...

「このリリースは Google Play の 64 ビット要件に準拠していません」

はい,またこのエラーがでました.64ビット対応できてないらしい.
調べていたらこの記事に出会った.

ほう,X86を入れてビルドするとだめらしい...

"X86"を外してビルドする.

  • "x86"のチェックマークを外す スクリーンショット 2019-11-22 16.51.41.png

これをビルドしたら無事に上げることができました!

これをすると32bitで動いていたものが動かなくなりますが,まぁしょうがないでしょう.

所感

ARM64にチェックマークを入れてね,という記事はあったが,x86を選択するとだめというトラップに見事引っかかってしまった.Unity 2019以降だとそもそもx86がないのでこういうことが起こってしまったのだろう.

しかし無事に対処できて本当に良かった.

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

【Android】動的Layout生成に潜む罠

ついにきましたね、[CA Tech Dojo/Challenge/JOB Advent Calendar 2019]、初日は@hohohorisが書かせていただきます。

自分は、2019年8月にCA Tech Dojo(Kotlin編)に参加させていただいた後、10月にCA Tech JOBでCATS(CyberAgent Advanced Technology Studio)でAndroidエンジニアとしてインターンをさせていただきました。

詳しくはインターン参加記を書いたので是非。

CA Tech Dojo(AndroidアプリKotlin編)という最高のインターンで最優秀賞をもらった話
【CA Tech JOB】CA Tech Dojoからの成長

概要

この記事ではAndroid開発で避けては通れないLayout周りの話をします。
個人アプリでCloud Firestoreを使おうとしていたのでその話をしたかったのですが、諸々間に合わず初日からニッチ目な記事を書きます...。

タイトルにある通り、Androidで動的にレイアウトを生成する際に自分がつまづいたのでそのときに得られた知見を共有したいと思います。

例えばFragment内のDataBindingのinflateの際も割と脳死で

HogeFragment
    override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
      binding = DataBindingUtil.inflate(context, R.layout.fragment_hoge, container, false)
      return binding.root
    }

って書いてるだけの人、いると思います。

以前の自分です。

このinflateの方法、全てに通用すると思っていませんか?意味、わかってますか?
その辺りを書いていきたいと思います。

基本的にLayoutの生成全般に関わることですが、よく使われているDataBindingにフォーカスしていきます。
ので、Android初心者/Androidを書いたことがない読者にもわかるようDataBindingとは、から説明します。

DataBinding

DataBindingは、MVVMと相性の良い技術で、レイアウトのViewとオブジェクトのデータを紐づける機能です。
MVVMやその他アーキテクチャの共通している思想として、「責務の分離」があります。
UIの表示をするレイヤーと、ロジックを記述するレイヤーを分けることにより、テストがしやすく、それぞれの責務が明確なため運用もしやすくなるメリットがあります。
その際、データソースとUIとの紐づけを完結に記述できるライブラリがDataBindingなのです。

beforeとafterでサンプルコードを見てみるとわかりやすいと思います。

before

DataBinding導入前

activity_main.xml
  <TextView
   android:id="@+id/txtName"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
  />
MainActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel.user.observe(this, Observer { 
            textName.text = it.name
        })
    }

コードは端折っていますが、ViewModelの何かしらのメソッドによりLiveDataなuserに更新があったらxmlのtextを変更するというシンプルなコードです。

after

上記コードにDataBindingを適用すると、

activity_main.xml
<layout>
    <data>
        <variable
            name="viewModel"
            type="com.example.MainViewModel"
        />
    </data>
    <!-- 略 -->
       <TextView
         android:id="@+id/txtName"
         android:text="@{viewModel.user.name}"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
       />
    <!-- 略 -->
<layout>
MainActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = viewModel
    }

xmlにデータソースとなるオブジェクトをvariableとして持たせ、インスタンスはActivity/Fragmentで渡してあげます。
xmlに渡したデータソースに変更があれば、@{}で囲んだ箇所が動的に更新されるという素敵なことができます。

コード量が少ないと恩恵がわかりづらいですが、setTextをする必要がないため、コードが綺麗になります。
他にも、BindingAdapterや双方向バインディングなど便利な機能が備わっているのですが、この記事のスコープ外なので気になるひとは調べてみてください。

DataBindingによるLayout生成方法

本題に入ります。
上記で、DataBindingによりlayoutを生成したのですが、これにはいくつか種類があります。

val binding = DataBindingUtil.setContentView(activity, R.layout.hoge)
//②と`②は同義。
val binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.hoge, container, false)
`②val binding = HogeBinding.inflate(LayoutInflater.from(context), container, false)
//③と`③は同義。
 val binding = HogeBinding.bind(View.inflate(context, R.layout.hoge, container))
`③val binding = DataBindingUtil.bind<HogeBinding>.bind(View.inflate(context, R.layout.hoge, container))

ハマりどころ

Activityなら①、Fragmentなら②を使うことが多いのかなーという印象で、
なかなか、これらの生成方法の違いを意識して記述することはないかもしれません。

ただRecyclerViewのitemでDataBindingを使いたい時、DialogでDataBindingを使いたい時、CustomViewでDataBindingを使いたい時、これらの違いを意識していないで雰囲気でコードを書くことで落とし穴にはまってしまうかもしれません。

自分、ハマりました。
コンパイルエラーが生じたり、ランタイムでViewが表示されないことがありました。

ポイントとなるのは、レイアウトの生成と表示は別問題であることです。
inflatebindメソッドでよしなに生成と表示をしてくれることもありますが、本質的には別問題です。

Viewやレイアウトの生成時に引数として渡すparentattachToRootに注目しながら、実際にcommand+Bで内部実装を軽くみてみましょう。

DataBindingUtil#inflate

我々がDataBindingを利用する際によく使うこのinflateメソッドからみていきます。
説明の都合上、一旦FragmentでのDataBindingにフォーカスします。

基本的にはFragmentではonCreateViewメソッド内で②のようにDataBindingの初期化を行いますが、ここで第3引数にViewGroupであるcontainerを渡し、第4引数にfalseを渡しています。
ここで内部実装にコメントで記載がある引数の説明を読んでみます。

     * @param parent Optional view to be the parent of the generated hierarchy
     *               (if attachToParent is true), or else simply an object that provides
     *               a set of LayoutParams values for root of the returned hierarchy
     *               (if attachToParent is false.)
     * @param attachToParent Whether the inflated hierarchy should be attached to the
     *                       parent parameter. If false, parent is only used to create
     *                       the correct subclass of LayoutParams for the root view in the XML.

ざっくり翻訳すると、

  • attachToParentがtrueであるとき、第3引数で渡したparentは生成されたView(layout)の親になる
  • attachToParentがfalseであるとき、第3引数で渡したparentは生成されたViewのLayoutParamsの決定のみに使用される

という感じです。

公式リファレンスでも記載があるように、FragmentはView自体をattachしないほうが良いので、第4引数にfalseを渡しています。
image.png

Fragmentの場合はLayoutをinflateした後に、onCreateViewの戻り値としてViewGroupを返すことでLayoutを表示させます。
これが、「レイアウトの生成と表示は別問題」と記述したところに繋がっており、戻り値にnullを指定するとFragmentは表示されません。

DataBindingUtil#bind

このメソッドを直接呼ぶことは少ないかもしれません。ただ、使えてしまうので、違いを知った上で安全に運用することはDataBindingに寄らずLayout生成周りで役に立つはずです。

この場合、引数にはrootとなるViewを渡してあげる必要があります。
その際にView#inflateを使用したのですが、こちらは第3引数にparentを渡しますが、DataBindingUtil#inflateにあったattachToParentがありません。
これは、同じく内部実装の引数説明を読んでみると詳細がわかります。

     * @param root A view group that will be the parent.  Used to properly inflate the
     * layout_* parameters.

こちらは、問答無用に引数で渡したparentがLayoutの親になってしまうようです。
そのため、③に記載したbindingの初期化方法は間違いとわかります。FragmentはLayout自体をparentに追加するのは正しくないからです。

Fragment

Fragmentの場合は上述した通りです。

Adapter

前述はFragmentでのDataBindingにおけるLayout生成にフォーカスしましたが、AdapterのitemをDataBindingで生成するにはどうすれば良いでしょうか。

これもLayoutのを生成したあと「どのようにして表示するか」で考えれば良さそうです。
それによってparentを渡すかどうか、渡したparentにattachするかどうかが変わります。

Adapterの場合はinflateしたViewをViewHolderのコンストラクタの引数に渡してあげることで表示ができます。
そのため、AdapterでDataBindingを使う時もparentに直接attachする必要はなく、②の方法でinflateができます。

RecyclerViewの場合は確認していないのですが、数年前にListViewで書かれたAdapterにDataBindingを導入した際に誤って

val binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.item_hoge, parent, true)

と、parentにattachしてしまい、

android.view.InflateException: addView(View, LayoutParams) is not supported in AdapterView

なるExceptionに遭遇しました。

そもそもgetViewで渡されてくるparentは多くの場合ListViewなのですが、ListViewはそもそもViewGroupを継承していないためaddViewができないためです。

itemをDataBindingで生成したあとにはparentにattachせずに、getViewの戻り値としてbinding.rootを返すことで表示しないといけないのです。

RecyclerViewはViewGroupを継承してはいるものの、表示方法としてはparentにattachするのではなくRecyclerView.ViewHolderにbinding.rootを渡してあげるほうが良いでしょう。

CustomView

これまで、Layoutを生成した後に表示する方法が別に用意されていたため、attachToParentfalseにしていたのですが、CustomViewの場合には注意が必要です。

CustomViewの場合は、よしなに表示してくれる機構が備わっていないため、これまでのようにattachToParentfalseにしてしまうと、Layoutは生成されているが、宙ぶらりんな状態になってしまいます。

どうするかというと、

MyCustomView.kt
class MyCustomView : FrameLayout {

    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
        val binding = ViewMyCustomBinding.inflate(LayoutInflater.from(context), this, true)
    }
}

こんな感じで書いてあげます。
parentthisを渡していますが、これはMyCustomViewが継承しているFrameLayoutです。
そして今まではfalseにしていたattachToParenttrueにしてあげることで生成したLayoutをViewGroupのヒエラルキーにいれてあげるのです。

まとめ

普段あまり意識しないようなLayoutの生成に関して、内部実装を元に違いをみてみました。
自分はAndroid歴があと2ヶ月で1年になるのですが、
「なんかしらんけど動くw」
の状態は脱して、
「こういう機構で動いていたのか」
というのを意識するようにしています。

Advent Calendar1日目からマニアックめなテーマで記事を書きましたが、
これを機に内部実装ちょこちょこ見るようにするとワンランクレベルアップできるかもしれないので是非!

ほりすでした。

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

Android LibraryをGitHub ActionsでビルドしGitHub Packagesで公開する

この記事はZOZOテクノロジーズ #1 Advent Calendar 2019 1日目の記事になります。

また、今年は全部で5つのAdvent Calendarが公開されています。

概要

先月のGitHub UniverseでGitHub ActionsとGitHub Packages(旧GitHub Package Registry)が正式リリースされました。GitHub PackagesはGitHubと統合されたパッケージのホスティングサービスで、ソースコードとその成果物であるパッケージを一括で管理できます。

integrated permissions management and billing

とあるので、package単位での課金がサポートされるかもしれませんね。

Supported clients and formatsを見るとGitHub Packagesは以下のクライアントとファイルフォーマットをサポートします。

Package client Language Package format Description
npm JavaScript package.json Node package manager
gem Ruby Gemfile RubyGems package manager
mvn Java pom.xml Apache Maven project management and comprehension tool
gradle Java build.gradle or build.gradle.kts Gradle build automation tool for Java
docker N/A Dockerfile Docker container management platform
nuget .NET nupkg NuGet package management for .NET

Apache Mavenのpom.xmlフォーマットに対応しており、また、クライアントとしてGradleをサポートしています。したがって、GitHub PackagesにAndroid Libraryをホスティングし、Androidプロジェクトから参照することができそうです。

この記事では、Android Libraryの作成、GitHub Packagesへの公開設定とGitHub Actionsを使用した自動アップロード、そして公開したライブラリのAndroidプロジェクトからの利用方法についてまとめます。

サンプルコード

https://github.com/horie1024/github-packages-android-sample

作業環境

Android Studioのバージョンは3.5.2を使用しています。

Android Libraryの作成

ライブラリを作成してみます。次のようなHelloクラスを作成しライブラリとして公開してみます。

Hello.kt
class Hello {
    companion object {
        fun world(name: String) = "Hello World ${name}!"
    }
}

使い方は次の通りです。

Hello.world(name = "Horie1024") // Hello World Horie1024!

プロジェクトの作成

Android Developersのプロジェクトの作成を参考にプロジェクトを作成します。

Android Studioのメインメニューから [File] > [New] > [New Project]を選択します。Create New Projectウィザードが表示されるので、「Empty Activity」を選択してNextをクリックします。

image.png

プロジェクトの設定をします。各項目の詳細は「プロジェクトを設定する」を参照してください。Finishをクリックするとプロジェクトが作成されます。

image.png

Libraryモジュールの作成

Libraryモジュールを作成します。ライブラリのコードは、Libraryモジュールに書いていきます。Android DevelopersのAndroid ライブラリの作成を参考にLibraryモジュールを作成します。

Android Studioのメインメニューから [File] > [New] > [New Module]を選択します。Create New Moduleウィザードが表示されるので、「Android Library」を選択してNextをクリックします。

image.png

ライブラリの名前、コードのMinimum SDKバージョンを指定して[Finish] をクリックします。

image.png

「mylibrary」というLibraryモジュールが作成されました。

image.png

appモジュールからの参照

ライブラリの開発を行う場合、appモジュールから参照できた方が便利です。Libraryモジュールを作成するとsettings.gradleが次のように更新されていることを確認します。

settings.gradle
include ':app', ':mylibrary'

appモジュールのbuild.gradledependenciesにブロックに次の行を追加します。

app/build.gradle
dependencies {
    implementation project(":mylibrary")
}

これでappモジュールからLibraryモジュールのコードを参照できるようになり、次のように使用できます。

MainActivity.kt
import com.horie1024.mylibrary.Hello

class MainActivity : AppCompatActivity() {

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

        hello_world.text = Hello.world(name = "Horie1024")
    }
}

ビルドしてアプリを起動するとこんな感じです。

image.png

GitHub Packagesへの公開設定

作成したライブラリをGitHub Packagesへアップロードして公開します。公開するための設定はConfiguring Gradle for use with GitHub Packagesにまとめられています。必要になる設定は次の2つです。

  • 認証設定
  • ライブラリの公開設定

Maven Publish Pluginを使用して設定しますので、Libraryモジュールのbuild.gradleの先頭にapply 'maven-publish'を追加します。

mylibrary/build.gradle
apply 'maven-publish'

認証設定

GitHub Packagesでのライブラリの公開、ダウンロード、削除には認証が必要になり、GitHubのユーザ名とAccess Tokenが必要になります。

Libraryモジュールのbuild.gradleにpublishingブロックを定義し、そこに設定を追加します。urlはライブラリの公開先で、https://maven.pkg.github.com/OWNER/REPOSITORYの形で指定します。そして、credentialsusenameにGitHubのユーザー名、passwordにAccess Tokenを指定します。

mylibrary/build.gradle
publishing {
    repositories {
        maven {
            name = "GitHubPackages"
            url = uri("https://maven.pkg.github.com/horie1024/github-packages-android-sample")
            credentials {
                username = project.findProperty("gpr.user") ?: System.getenv("USERNAME")
                password = project.findProperty("gpr.token") ?: System.getenv("TOKEN")
            }
        }
    }
}

ライブラリの公開設定

GitHub Packagesがサポートするpackageのフォーマット(pom.xml)にライブラリのソースコードを変換する必要があります。

publishingpublicationsブロックを定義し、ライブラリの公開設定を追加します。ここで①の設定を書かない場合、ライブラリの推移的依存関係が含まれなくなります。

mylibrary/build.gradle
apply 'maven-publish'

task sourceJar(type: Jar) {
    from android.sourceSets.main.java.srcDirs
    archiveClassifier.set("sources")
}

publishing {
    publications {
        maven(MavenPublication) {
            groupId "com.horie1024"
            artifactId "github-packages-sample-library"
            version "$VERSION"
            artifact sourceJar
            artifact "$buildDir/outputs/aar/mylibrary-release.aar"

            // include any transitive dependencies・・①
            pom {
                withXml {
                    def dependenciesNode = asNode().appendNode('dependencies')

                    project.configurations.implementation.allDependencies.each {
                        if (it.group != null || it.name != null || it.version != null || it.name == "unspecified") return

                        def dependencyNode = dependenciesNode.appendNode('dependency')
                        dependencyNode.appendNode('groupId', it.group)
                        dependencyNode.appendNode('artifactId', it.name)
                        dependencyNode.appendNode('version', it.version)
                    }
                }
            }
        }
    }
    repositories {
        maven {
            name = "GitHubPackages"
            url = uri("https://maven.pkg.github.com/horie1024/github-packages-android-sample")
            credentials {
                username = project.findProperty("gpr.user") ?: System.getenv("USERNAME")
                password = project.findProperty("gpr.token") ?: System.getenv("TOKEN")
            }
        }
    }
}

また、ライブラリの公開設定はwupdigital/android-maven-publishを使用するとより簡単に記述できます。ライブラリを使用する側から、ライブラリのKotlinのソースコードを追うことができなかったので今回は使用を見送っています。

手動での公開

Access TokenはGitHubの[Settings] > [Developer settings] > [Personal access tokens] から作成します。scopeはrepowrite:packagesread:packagesにチェックを付けて生成します。

image.png

ユーザー名とtokenをgradle.propertiesに次のように定義するか、環境変数として定義します。

gradle.properties
gpr.user = horie1024
gpr.token = 123456789abcdefghijklmnopqrstuvwxyz

次のコマンドを実行するとGitHub Packagesへライブラリが公開されます。

./gradlew assembleRelease publish

そして、公開が成功するとRepositoryのpackagesページに次のようなページが追加されます。これで作成したライブラリを公開できました。

image.png

GitHub Actionsの設定

手動での公開ができたのでGitHub Actionsで自動化しましょう。

今回はmasterブランチにコミットがpushされたタイミングでGitHub Packagesにライブラリが公開されるようにします。これは次のymlをプロジェクトトップの.github/workflows以下に配置することで実現します。

publish_library.yml
name: "Publish library to GitHub Packages"
on:
  push:
    branches:
      - master

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: Publish library
      env:
        USERNAME: horie1024
        TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: ./gradlew assembleRelease publish

triggerの設定

masterブランチにコミットがpushされたタイミングでワークフローがトリガーされて欲しいので、onで制御します。次のように設定することでmasterブランチにpushされた場合にワークフローをトリガーすることができます。

on:
  push:
    branches:
      - master

jobの設定

ワークフローはjob単位で実行されていきます。ここではpublish jobを定義します。

runs-onはjobを実行する環境でubuntu-latestを指定します。stepsはjobを構成する一連のタスクです。1つ目のstepではuses: actions/checkout@masterでソースコードをチェックアウトし、2つ目のPublish library stepでライブラリの公開を実行します。

Publish library stepでは、envでstep内でのみ有効な環境変数を定義しています。ここでTOKENにはsecrets.GITHUB_TOKENを指定します。GITHUB_TOKENはワークフロー内で自動的に作られる環境変数です(詳細はこちら)。また、GITHUB_TOKENにどのようなパーミッションが与えられているかはこちらから確認でき、GitHub Packagesについてもread/writeパーミッションを持っています。

そして、run./gradlew publishを実行することでライブラリが公開されます。

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: Publish library
      env:
        USERNAME: horie1024
        TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: ./gradlew assembleRelease publish

公開したライブラリの利用

GitHub Packagesで公開したライブリを利用するには、認証設定が必要です。現時点(2019/12/01)でpublicなrepositoryの場合でも認証が必要になります。

認証設定

プロジェクトトップのbuild.gradle内、allprojectsブロックのrepositoriesブロックにライブラリの公開設定と同様の設定を追加します。

build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext.kotlin_version = '1.3.60'
    repositories {
        google()
        jcenter()

    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()

        // repositoriesにGitHub Packagesを追加
        maven {
            name = "GitHubPackages"
            url = uri("https://maven.pkg.github.com/horie1024/github-packages-android-sample")
            credentials {
                username = USERNAME // GitHubのユーザ名
                password = ACCESS_TOKEN // 発行したAccess Tokenを指定
            }
        }
    }
}

Access Tokenは、read:packages scopeを指定して作成します。用途としてはライブラリをダウンロードするだけなのでscopeは絞った方が良いでしょう。

image.png

dependenciesへの追加

dependenciesブロックにライブラリを追加し、「Sync Project with Gradle files」を実行すると公開したライブラリを使用できるようになります。

app/build.gradle
android {
    // androidブロックの定義
}

dependencies {

    // 公開したライブラリをimplementationに追加
    implementation 'com.horie1024:github-packages-sample-library:1.0.0'
}

まとめ

Androidのライブラリ作成してGitHub Packagesで公開してみました。GitHub Actionsと組み合わせるとライブラリの自動公開まで簡単に実現でき、今後社内向けのAndroidライブラリはGitHub Packagesでホスティングしていこうと考えています。

明日は@kenz_firespeedさんによる「今風の画像アップローダーを作る」です。

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

Android Webkitまとめ (概要から最新AndroidXまで)

1. はじめに

Android Webkitについての概要とAndroid OSと絡めた歴史的な経緯、そして最後にAndroidX Webkitについてまとめています。

2. WebkitとWebViewについて

まず、WebkitとWebViewについて概要などを説明します。

2.1. WebViewとは?

この記事を読んでいる方には説明不要かと思いますが、WebViewとはAndroidアプリケーションからHTMLやWebページ等のHTMLファイルをレンダリング(表示)する機能を提供してくれるViewのことです。

Android公式サイトのドキュメントはこちらです。

2.2. Webkitとは?

サポートライブラリで、WebViewおよびWebブラウジングに関する機能を提供してくれるライブラリのことです。

Android公式サイトのドキュメントはこちらです。

Android Webkitは、実質WebViewの中核技術であるレンダリングエンジン (ネイティブ側の.soファイルとして存在) をバインディングしたラッパーAPIとなっており、WebViewやブラウザ相当の機能を実現するために必要な機能を提供してくれます。この辺の機能を駆使するとブラウザアプリが作れます。

2.3 WebViewレンダリングエンジン

WebViewのレンダリングエンジンは以下に示すように、Android OS 4.4を境に変化しています。

Android OSバージョン WebViewレンダリングエンジン
〜4.3 WebKit
4.4〜現在 Blink

WebKitとは、主にSafariで利用されているOSSのレンダリングエンジンです。

一方、Blinkとは、ベースは前述のWebKitですが、GoogleがJavaScriptコアをV8エンジンに変更したり、大幅に作り替えたChromeで利用されているレンダリングエンジンです。マイクロソフトがWebKitからBlinkに乗り換えてEdgeブラウザを開発しているなどのことを考えると、Blinkが最も勢力図で勢いがあると考えて良いでしょう。

2.3.1 [参考] HTMLレンダリングエンジンの勢力図

世の中の勢力図的には、GoogleのChrome (Blink) 系が勢いがありますね。

image.png

なお、ChromeはGoogle製ブラウザですが、OSSのブラウザとしてChromiumというものがあります。Chromiumとは、Googleが開発しているGoogleカスタマイズなしの素のChromeと思ってもらって良いです。GoogleはChromiumというOSSでコア部分を開発し、そこに独自の非公開の味付けをしたものをChromeブラウザとしてリリースしています。

Chromiumについての情報やソースコードなどはこちらを参照してください。

2.4. WebViewの歴史的遷移

前述の通り、WebViewのレンダリングエンジンはAndroid 4.4からBlinkに変更になっていますが、それ以外にもWebViewの配布方法やバインディング方法等が歴史的に変わっています。ここを説明します。

WebViewのバインディング先は以下のようにAndroid OSバージョンがUpする度に変更されています。

Android OSバージョン WebViewバインディング先
〜4.4 Android OS組み込み
〜6.0 System WebViewから独立し、apkで独立配信されPlayストアで更新になりました。WebViewコアのBlinkはセキュリティ含めて更新頻度が高いため、Android OS組み込みは非現実的のため、このような手段が取られています。
7.0〜 ChromeがWebView機能を提供してくれるようになり、Chrome or System WebView(Chrome優先)が開発者メニューで選択可能となりました
10 System WebViewのみ。開発者メニューからChrome項目が削除されました

2.5. WebViewの実装メニュー

開発者向けオプションのWebViewの実装からAndroid System WebViewかChromeかを選択できますが、前述の通り、Androi10からはChromeが削除されています。

Screenshot_20191223-182545.png

Android10では、System WebViewとChrome WebViewが共通化され、Trichromeと呼ばれる形で提供されるようになっているらしいです。ChromeアプリがWebView機能をSystem WebView側を利用するようになったということでしょうか?
https://chromium.googlesource.com/chromium/src.git/+/master/docs/android_native_libraries.md

3. AndroidX Webkit

ここからJetpackの一つである、AndroidX Webkitについて説明します。AndroidX WebkitはSupport Libraryの置き換え以外にも、AndroidX Webkitにのみある新機能があります。しかし、今時点では発展途上でAndroid Webkitの完全な置き換え可能なレベルには到達していません。

AndroidX Webkitの簡易アーキテクチャは以下のような感じです。
image.png

3.1. 新機能

いくつか便利な機能があるため、紹介します。

3.1.1. WebViewFeature

APIバージョンコードを気にせず、機能有無でコードを書けます。

従来のコード

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
    WebView.startSafeBrowsing(this.applicationContext) { value ->
        Log.d(TAG, "WebView.startSafeBrowsing: $value")
    }
}

WebViewFeatureを使ったコード

if (WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {
    WebViewCompat.startSafeBrowsing(this.applicationContext) { value ->
        Log.d(TAG, "WebViewCompat.startSafeBrowsing: $value")
    }
}

3.1.2. ProxyController

プロキシ設定を簡単に出来ます!

if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
    // proxy1.comが失敗したら、次はproxy2.comのように上から順番に試していく
    val proxyConfig = ProxyConfig.Builder()
        .addProxyRule("proxy1.com")
        .addProxyRule("proxy2.com", ProxyConfig.MATCH_HTTP)
        .addProxyRule("proxy3.com", ProxyConfig.MATCH_HTTPS)
        .addBypassRule("www.google.*") // プロキシ設定除外のホスト
        .build()

    // Executor
    val executor = Executor { Log.d(TAG, "${Thread.currentThread().name} : executor") }

    // プロキシ設定変更が受付された時に呼ばれる?呼ばれないような…
    val listener = Runnable { Log.d(TAG, "${Thread.currentThread().name} : listener") }

    // WebViewのプロキシ設定をシステム設定から上書き
    ProxyController.getInstance().setProxyOverride(proxyConfig, executor, listener)

    // システムの設定に戻す
    ProxyController.getInstance().clearProxyOverride(executor, listener)
}

3.1.3. ダークモード設定 ※webkit:1.2.0からの機能

WebViewのダークモードの設定が出来るようになるらしいです。

image.png

if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
   //WebSettingsCompat.setForceDark(webView.settings, WebSettingsCompat.FORCE_DARK_AUTO)
    //WebSettingsCompat.setForceDark(webView.settings, WebSettingsCompat.FORCE_DARK_OFF)
    WebSettingsCompat.setForceDark(webView.settings, WebSettingsCompat.FORCE_DARK_ON)
}

3.2 使い始めるために

Build.gradle
dependencies {
    implementation androidx.webkit:webkit:1.1.0
}

最新は1.2.0-alpha01があります。ダークモードを試したい方はまずはこれを使うと良いでしょう。

3.3 サンプルコード

githubに、まずはそのまま動かすレベルのサンプルコードを用意していますので、必要であればご利用下さい。

4. 参考文献

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

Android Edge-to-edge入門

この記事は概ね、Gesture Navigation: Going edge-to-edge (I)Gesture Navigation: Handling visual overlaps (II)の記事に沿って書いています。


Android Qから新しいシステムナビゲーションが追加されました。
iOSではおなじみですが、ボタンではなくジェスチャーによって前の画面に戻ったりAndroidのホーム画面に遷移することが可能になりました。
gesture.gif
ジェスチャー ナビゲーション: エッジ ツー エッジへの対応より

このジェスチャーナビゲーションを利用することで、従来の3ボタンナビゲーションよりも多くの描画領域をアプリに提供できるため、より没入感のあるUXを提供することができるようになりました。

Edge-to-edgeとは

いわゆるこれです。
edgetoedge.gif
ジェスチャー ナビゲーション: エッジ ツー エッジへの対応より

よりアプリへの没入感の高いUXを提供できる表現のことをこう呼びます。
より詳しくはぜひGoogle Developersのブログを御覧ください。

Gesture Navigation: Going edge-to-edge (I)に関しては日本語訳記事もあるので一読すると理解が深まります!

実装方法

前提

本記事はandroidx.core:core-ktx:1.2.0-rc01を用いてコードの検証を行っています。
また、コードの中でデータバインディングを用いている箇所があります。

dependencies {
    ...
    implementation 'androidx.core:core-ktx:1.2.0-rc01'
}

題材として、適当な文字列をリストで表示。またFloatingActionButtonのある以下のようなUIをEdge-to-edge対応させていく流れで進めてみます。
2019-11-30 22.54.21.png
普通に作るとこんな感じ。コードはすべてこちらに。
左はAPI 27、右はAPI 29です。
ブランチを変えるとそれぞれのステップに対応しています。

全画面表示に対応する

まずはステータスバーの背後にアプリを描画できるようにしましょう。
これ自体は非常に簡単で、systemUiVisibilityにflagを設定してあげるだけです。

MainActivity.kt
view.systemUiVisibility =
    View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

スクリーンショット 2019-11-30 22.59.14.png
善し悪しは置いておいて、ステータスバー・ナビゲーションバーの背後にUIが配置されています。

ステータスバーの背後のみや、ナビゲーションバーの背後のみということも可能です。
自分のアプリに適した設定にしましょう。

システムバーの色を変える

次に重なって表示されたUIを見えるようにするため、透過された色をシステムバーに設定しましょう。
Android Qでは単純に透明に設定するのみでよいです。システムが動的に色を調整してくれます。
Pie以前では動的な色の調整は行われないため、半透明に設定することが推奨されています。
その際、不透明度が70%から始め、各自のアプリに表示されるコンテンツに応じて調整することが推奨されています。

resources.xml
<!-- styles.xml -->
<resources>
    <style name="Base.App" parent="Theme.MaterialComponents.Light.NoActionBar">
        ...

        <item name="android:navigationBarColor">@color/nav_bar</item>
        <item name="android:statusBarColor">@android:color/transparent</item>
    </style>
</resources>

<!-- values/colors.xml -->

<resources>
    ...

    <color name="light_scrim">#B3FFFFFF</color>
    <color name="nav_bar">@color/light_scrim</color>
</resources>

<!-- values-v29/colors.xml -->
<resources>
    <color name="nav_bar">@android:color/transparent</color>
</resources>

スクリーンショット 2019-11-30 23.13.59.png
描画領域が広がったことがわかるかと思います。

視覚的な重なりに対応する

最後に、システムバーの背後に描画されたUIについて適切に対応していきましょう。肝です。

たとえば、FloatingActionButtonを使っている場合、ボタンがナビゲーションバーと重なってしまっていることに気づくでしょう。
あるいは、このようにリストを表示している場合、リストの最後の要素がナビゲーションバーに重なってしまい、見づらくなってしまっています。

よくない対応

よくない対応として、それぞれのマージンあるいはパッディングにシステムバー分の高さを足すパターンがあります。
システムバーはAndroidのバージョンや端末により異なる場合場あるので、そこを考慮していない場合は表示が崩れることになります。今後のことを考えても、これは避けたほうが良いでしょう。

スクリーンショット 2019-11-30 23.15.24.png
これは、layout_marginBottomを、ナビゲーションバーの48dpとFABのマージンの16dpを足して、64dpとした時のスクリーンショットです。
API 27では一見良さそうですが、API 29でみると崩れていますね。

適切な対応

System window insetsを利用し、適切に対応していきましょう。
WindowInsetについては詳しく説明しません。こちらに非常に丁寧に説明されているのでぜひ御覧ください。

ViewCompatsetOnApplyWindowInsetsListenerからWindowInsetにアクセスできるので、ここでViewのマージンあるはパッディングを調整しましょう。

MainActivity.kt
private fun handlingInsets(view: View) {
    // リソースからマージンを取得
    val fabMargin = resources.getDimensionPixelSize(R.dimen.fab_margin)
    // XML等から既にviewで指定されているpaddingを取得
    val listBottomPadding = binding.listText.paddingBottom
    // WindowInsetsにアクセス
    ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
        // マージンを更新するときはこんな感じ
        binding.toolbar.updateLayoutParams<AppBarLayout.LayoutParams> {
            topMargin = insets.systemWindowInsetTop
        }
        binding.fab.updateLayoutParams<CoordinatorLayout.LayoutParams> {
            leftMargin = fabMargin + insets.systemWindowInsetLeft
            rightMargin = fabMargin + insets.systemWindowInsetRight
            bottomMargin = fabMargin + insets.systemWindowInsetBottom
        }
        // パッディングを更新するときはこんな感じ
        binding.listText.updatePadding(
            bottom = insets.systemWindowInsetBottom + listBottomPadding
        )
        insets
    }
}

スクリーンショット 2019-11-30 23.36.57.png
WindowInsetsがバージョン毎のサイズを適切に判断してくれるので、それぞれきれいにマージンが指定できました。

その他の対応方法

上記で説明した流れは、Insetterというライブラリで一貫して対応が可能です。
ここまで説明しといてなんですが、かなり便利なのでこちらを使うのもオススメです。

簡単に解説したスライドを公開しているので、参考までに。

まとめ

・入門は簡単だけど、それぞれでWindowInsetsに対応するのに骨が折れそう
・UXは高まるので対応していきたい。

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

Delphi で Pull To Refresh

PullToRefresh

Delphi で簡単に PullToRefresh しようよ~(ジョイマン風に)

PullToRefresh とは

上から下に引っ張ると表示内容が更新される機能です。

ListView PullToRefresh

TListView には PullToReflesh が機能として提供されています。

PullToRefresh の所にも書いてありますが、使い方は

  1. PullToRefresh プロパティを True にする
  2. OnPullRefresh イベントハンドラに更新する内容を記載する
  3. 更新が終わったら StopPullReflesh を呼ぶ

という感じです。

Object.png

OnPullRefresh
procedure TfrmMain.ListView1PullRefresh(Sender: TObject);
begin
  CreateRandomValue; // ListView にランダムな値を設定するメソッド
  ListView1.StopPullRefresh; // iOS の場合に待機アニメーションが停止される
end;

また PullRefreshWait というプロパティがあります。
このプロパティは iOS でしか効果がありません。
このプロパティを True にすると待機アニメーションが自動的に止まるようになります。
このプロパティを False にした場合は、コードで示したように StopPullRefresh を手動で呼び出します。
本来、データの更新というのは時間が掛かる物なので、PullRefreshWait を True にする機会は少ないかも知れません。

実際に動かすとこんな感じ

iOS
P2RSample.gif

Android
Xperia.gif

ListView 以外ではどうする?

ここからが本題!
ListView 以外の時にこの動作をさせたくても、そんなコントロールは無いのでした。

ということで Top に TLayout を置いて OnMouseDown, OnMouseMove, OnMouseUp で引っ張り出せるようにしてみました。
image.png

利点

この方法の利点は全部の OS で使えるということです。
先に上げた TListView の方法だとモバイルだけ。しかも Android の表示はそもそも Pull 感が無くて超解りづらい…

欠点

引っ張り出せるようにするために上部に少しコントロールを出しておかないといけない、というところです。
なので、画面上部にユーザーが操作する UI を置いておくことができません。

実装

これを実装した PK.GUI.PullToRefreshLayout を作成しました。
TPullToRefreshLayout を使うコードは↓こんな感じ

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  FP2RLayout := TPullToRefreshLayout.Create(Self); // FP2RLayout は TPullToRefreshLayout として定義
  FP2RLayout.MaxPullingLen := 80; // どこまで引っ張り出せるか

  FP2RLayout.BoundsRect := Layout1.BoundsRect; // "PullToRefresh Sample" とあるエリアと同じ位置にする
  FP2RLayout.Parent := Self;
  FP2RLayout.BringToFront; // 最前面に持ってきて触れるようにする

  Path1.Parent := FP2RLayout; // 矢印を表示している Path を載せる

  FP2RLayout.OnRefresh := P2RLayoutRefresh; // イベントハンドラ

  CreateRandomValue;
end;

procedure TfrmMain.P2RLayoutRefresh(Sender: TObject);
begin
  CreateRandomValue; // 更新
end;

コレを使うと↓のような動作になります!

Windows
P2RSample.gif

macOS
P2RSample.gif

iOS
P2RSample.gif

Android
P2RSample.gif

まとめ

元から用意されている場合はそれを使って、用意されていない場合は工夫することが大事ですね!

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