Androidチーム2021年の改善活動

この記事は Calendar for Akerun | Advent Calendar 2021 - Qiita の 11 日目の記事です.

Webエンジニアのohioshirt - Qiitaです。主にAndroidアプリの開発を担当しています。

Androidチームは、2020年に投稿されたHello legacy codes! - フォトシンス エンジニアブログ のようなレガシーコードの課題を抱えていました。
レガシーコード自体については、先の記事で詳しく触れていますので、
本記事ではAndroidチームがそれに向き合いやってきたことをざっと振り返ってみます。

主な取り組み

「コード」に特化するとネタが尽きるのでここではもう少し広く、レガシー体験の改善とします。

リリースビルドの自動化

これまではAndroid Studioで Build > Generate Signed Bundleを実行していました。
つまり手作業です。初めはビルドの度にドキドキしていました。 Play Console APIを直接叩く方法もありますが、 https://github.com/Triple-T/gradle-play-publisherで済ませました。
今ではgit tagを打つことでCircleCIでworkflowが走り、
Google Play Consoleにリリース用のBundleが上がるようになりました。
地味な手作業が減ることに加えて、ビルド環境も個人に依存しなくなり不安要素が減りました。
今後も手作業・属人化した作業から解放されるための取り組みを続けていきます。
私たちの戦いはこれからです。

f:id:photosynth-inc:20211216110220p:plain
リリース用のCircleCI workflowができました

ユニットテストの改善

Androidチームでは、Pull RequestなどをトリガーにCircleCIでユニットテストを実行するワークフローを組んでいますが、
一時期ユニットテストが途中で落ちる事象が続いていました。  

悲しみのメッセージたち
Too long with no output (exceeded 10m0s): context deadline exceeded
The connection attempt hit a timeout after 120.0 seconds (last known process state: STARTED, running: true). This exception might occur when the build machine is extremely loaded.
Process 'Gradle Test Executor 7' finished with non-zero exit value 137

f:id:photosynth-inc:20211216113001p:plain
タイムアウトしていました。
f:id:photosynth-inc:20211216130339p:plain
並列化せよとのお達し

CircleCIのサポートページ「ビルドに設定してあるタイムアウトリミットを超えた場合」に一部のエラーへの対処法が記載されていますが、
そうした一時凌ぎができる問題ではないことが分かったのでテコ入れを行いました。

  • テスト実行マシンのスペック見直し
    CircleCIのドキュメントによれば、
    resource-class: large 以上のマシンスペックを指定するのが一般的なようです。
    Androidチームではmediumを指定していました。おそらく導入当時はそれで足りていたのだと思います。
    迷わずlargeに上げました。

  • ユニットテストの並列化
    CircleCIはテストの並列実行をサポートしているため、Androidチームでもこれを導入しました。
    ただし、Androidプロジェクトではこのドキュメントのようにファイル毎の実行はできずモジュール毎のテスト実行が必要です。
    コンテナ毎のテスト実行対象モジュールはシェルスクリプトで制御、指定します。
    実行中のコンテナID(0〜i)は $CIRCLE_NODE_INDEXで取れるので、 n%i の要領で実行対象を割り振れます。

# feature/loginのような構成で置かれているモジュールに対してディレクトリを走査してリストを生成、forループで回して ./gradlew :login:testDebugUnitTest の形で実行する
for D in "${SEARCH_DIRS[@]}"; do
MODS="$(ls "${D}")"
  for MOD in $MODS; do
    MODULES+=("${D}:${MOD}")
  done
done
  • JaCoCoを利用したテストカバレッジの表示
    ユニットテストカバレッジもCIで取るようにしました。 https://github.com/arturdm/jacoco-android-gradle-plugin などでサクッとモジュール単位のレポート生成はできたのですが、
    CI上で並列実行されるテストレポートをどう結合すべきかが悩みどころでした。
    とりあえずの対処としては、ローカルで実行した状態を再現させることにします。
    モジュール毎のテストレポートファイルを一時保存し、
    テスト完了後に保存したファイルを元の位置に戻してレポートを結合するようにしました。
    動作イメージは以下のようなものになります。
# テスト job
- run: cp -r --parents ".*/build/reports" /tmp/reports
- persist_to_workspace:
     root: /tmp/reports

# テスト の後のjob
- attach_workspace:
     at: /tmp/reports
- run: cp -r /tmp/reports/* .
- run: ./gradlew レポート結合
- store_artifacts:
     path: ./build/reports

暫定対処である印象は否めないものの、妙な作り込みが少ないためひとまずはこれで運用しようと考えています。

f:id:photosynth-inc:20211216150120p:plain
改善後のCircleCIのテスト実行結果

諸々の改善によりユニットテスト実行時間が20分(時々落ちる)→平均7分前後にまで短縮されました。
カバレッジの表示については、具体的な数字やグラフが見えると達成感を得やすいので、 まだ導入していない方はぜひご検討ください。
今回の導入によりテストコードが薄い箇所が露見したため、これからも改善に取り組んでいきます。
私たちの戦いはこれからです。

Javaで書かれたコードのKotlin化、リファクタリング

新規に実装する時にKotlinを選ぶのは当然のこととして、
機能改修に合わせて既存のJavaコードをKotlinにリファクタリングするなどの改修を行ってきました。
どれだけ変わったのか、MADスコアを算出して1年前のコードと比較しました。
スコアの算出方法はこちらで紹介されていますので、
興味がある方はぜひ自分のプロジェクトでお試しください。

f:id:photosynth-inc:20211210135516j:plain
2020年のMADスコア(参考記録

f:id:photosynth-inc:20211210135616j:plain
2021年のMADスコア

地道な改善を続けた結果、Kotlinコードの割合が大きく変化しました。
機能追加・改修にあまり影響を与えないJavaコードはまだ残っています。
今後もビジネス要求に素早く応えるため、そして自分たちの開発体験をよくするための取り組みを続けていきます。
私たちの戦いはこれからです。

皆さんもこの1年間の成果を振り返ってみてはいかがでしょうか。


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com

エンジニアとコーチングの相性の良さ

この記事は Calendar for Akerun | Advent Calendar 2021 - Qiita の 10 日目の記事です。
FW 開発者だけどあまりコードを書く時間がとれていない AkiAbe - Qiita です。

テクニカルなことはギークなチームのみなさんが記載してくれるので、私からはヒューマンよりのお話を。

はじめに

ソフトウェアエンジニアって世間から見ると「パソコンと仕事をしている」みたいな印象を持たれること多そうですが、実はめちゃくちゃ人と対話をして協力しながら仕事を進める職業です。
だって、皆の叡智を結集して一つのプロダクトを作るんですもの。

この記事のタイトルに「コーチング」という題名がついていますが、コーチングのやり方とかそういうことには言及しません。
エンジニア (というか人類) が、人と一緒に何かを実現するときに、コーチングの考え方を知っておくことのメリットを挙げるだけの記事です。
また、コーチングは色々な考え方があります。あくまで、この記事は個人の解釈となります。ご了承ください🙇🏻‍♂️

コーチングとは

とはいえ、コーチングってどういうこと?という方もいると思うので、簡単に私の考える解釈を記載してみます。

コーチの語源

コーチ (Coach) は馬車を意味します。(同じ名前の某有名ブランドも馬車のマークですね)
コーチングは、「人を望む場所に送り届ける技術」という解釈をしています。

コーチングとティーチング

よく使うたとえで、飢えた人がいたときに

  • 魚を与える (答えを教えて今の問題を回避する) のが teach
  • 魚のとり方を教える (答えを自分で導くための術を教える) のが coach

というものがあります。

短期的な問題解決だけをする場合 (この例だと、とりあえずすぐに飢え死にするのは避ける) は teach の手法をとることも良いかと思います。
ですが、「人を育てる」、「自立を促す」という意味では coach のアプローチを取るのがよいと思います。

世界は対話でなりたっている

前述したとおり、エンジニアの仕事の大半は対話です。
コーチングもクライアントとの対話により気づきを与えていき成長を促していくようなスキルになります。
なので、エンジニアはコーチングを知るべきなのです。

IoT 機器なんかは機械同士で対話して情報をやり取りしているし、ソフトウェアもモジュール同士が決められたプロトコルで対話していますね。
我々も、プライベートではオフラインで家族やパートナーと対話をしていますし、スマホなどを通してオンラインで世界の誰かと常に対話して生活してますね。
ということで、やっぱり世界は対話でなりたっているんですよ。

私が学んで一番よかったこと

コミュニケーションタイプを知れたこと

「人により、コミュニケーションタイプというものがある」ということを知れたことです。
コミュニケーションタイプに関してはこの記事が読みやすいと思います。 タイプはざっくり 4 分類にわかれています。

coach.co.jp

相手のコミュニケーションタイプをあらかじめ知っていると、どういうコミュニケーションをとると話がスムーズに進みそうか?など見えてくる (気がする) 。

私のコミュニケーションタイプはなんだろう?

ちなみに私はプロモーターというタイプです。
ちょっと特性みていきましょう。

アイディアを大切にし、人と活気あることをするのを好む

うんうん。なんか活発に意見を交わす好青年の絵がみえますね。
いいですね。

承認を代表とする、こまめな働きかけがないと、一気にやる気を失います。
また、オリジナルなアイディアを大切にするため、自分のアイディアを否定されることを嫌います。

えっ。結構めんどくさいやつやん。。

彼らと接するときは、次のポイントを意識してみましょう。

  • 話を聞くときは相づちをうつなどして明確に反応する
  • 質問は間口の広い質問をして自由に話をさせる
  • たくさんほめる(特に彼らの影響力をほめると効果的)
  • 彼らのオリジナルなアイデアに耳を傾ける

だそうです。ほめて伸びるタイプ!!
みんな、もっとほめて!!!

まあこんな感じでざっくりとでも、対話をする人がどんなタイプかわかっていれば 、同じ内容を素早く伝えるために、伝え方を工夫することができます。

いい意味で「人に期待をしない」

もともと他人を期待をしない性質でしたが、この 4 タイプを知ってから
それがより自分の中の人付き合いの軸になりました。

「期待をする」という行為自体は悪いわけではないですが、「裏切られた」
と感じたときのストレスは、私にとってかなり大きいです。

とはいえ、私もたまに都合よく何かを期待をしてしまうことはあるのですが
「そもそも考え方 (というか大事にしているポイント) が違うんだから仕方ない」
と思えるようになったことで、生活のストレスが少し減りました。

そして、私もたぶん誰かの期待を裏切りながら生きている。
でも、叩かないでください。
ちゃんとそういう認識していることを、ほめてくださいw

「おもいやり」と「自衛」

前述の「期待しない」から派生している考え方なのですが
私自身の一緒に仕事する人への接し方が変わりました。

基本的な考え方としては

  • みんな知らなくて当然
  • 意見、反論があって当然
  • 対話が前提 ( 一発で話が伝わると期待をしない)

対話をするときも、話し相手が 4 つのタイプのどの傾向を持っているかを
なんとなく把握できると

「この人にはこういう接し方をした方が、素直に意見を受け取ってもらえそうだな」
というように、スムーズに話を進めるための思いやりができるようになります。

また、自分が嫌に思うような言い方をされたときでも
「この人がこういう言い方するのは、悪気はないんだな」
と思うことができるようになります。

こうなれば、無駄に自分が傷つくことを減らすこともできます。
自分が傷ついてしまうと、相手へのコミュニケーションの取り方も雑になってしまいますし
負の循環を構成する要因にもなると思っています。

コーチングを学ぶことは、相手を伸ばすことだけでなく、自分を守ることにもつながるのです。
こうみると、素晴らしい学問ですね。

さいごに

コーチングというワードは 「人を育てる」という目線や文脈での話題が多いと思いますが、今回は「自分を守り、自分が仕事をしやすくする」という目線で記事を書きました。

  • エンジニア (に限らず人類) って、人と対話をしながら、協力しながら仕事をしています
  • 自分と違う思考パターンの人は世の中に存在します
  • そういった人と話すときに誰も傷つけないことができると一番ハッピーです
  • でも、自分の人生なのでまずは自分が傷つかないことを優先しましょう
  • そのとっかかりとして、コーチングを知ることはメリットのあること

みたいなことが伝えたかったことになります。

当然、コーチングという技術は、自分が関係する人 (たとえば、部下や後輩) を成長させるために使えるもので、そのような使い方をするのにもめちゃくちゃ有意義です。
その辺りのお話はまた別途。

みなさん、これからも楽しく開発をしてものづくりをしていきましょう!
(あれ?結局、コミュニケーションタイプと、それを私がどう利用しているかの話しか書いてないな。。)


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com

ソフトウェア考古学者へ。Hack-The-Box はいいぞ

この記事は Calendar for Akerun | Advent Calendar 2021 - Qiita の 6日目の記事です。

昨日に引き続き、 daikw - Qiita です。最近、 Hack The Box に手を出しています。

昨日書いたソフトウェア考古学は実際に社内で行なっていますが、この業務に ペネトレーションテスト の経験が生きたなと感じました。今日はそんなお話です。

要約

  • 「よくわからないものを、よくわからないなりに、何とか扱う」という点で、 攻撃的セキュリティソフトウェア考古学 の構造はかなり似ている。
  • みんなで Hack-The-Box を勉強してつよいエンジニアになろう!

意義と建前

攻撃的セキュリティ (Offensive-Security) を学ぶことで、防衛に役立てる。もって事業継続を確かなものとする。

情報セキュリティを語るとき、リスク脅威守るべき資産守るための活動 などに分けて考えることができます。

インターネット上の全ての主体は、常に何らかの 脅威 に晒されていることが知られています。 守るべき資産 が脅威に晒されるとき、 リスク が生じます。

リスクに対してノーガードでは一瞬で首を刈られます。リスクを知り、緩和し、資産を 守るための活動 を一定行う必要があります。

脅威やリスクのカテゴライズは色々な団体が行っており、例えば 情報セキュリティ10大脅威 2021:IPA 独立行政法人 情報処理推進機構 から引用すると*1、 組織外部の主体的な脅威(ランサムウェア・標的型攻撃)が上位に来ています。

昨年順位 個人 順位 組織 昨年順位
1位 スマホ決済の不正利用 1位 ランサムウェアによる被害 5位
2位 フィッシングによる個人情報等の詐取 2位 標的型攻撃による機密情報の窃取 1位
7位 ネット上の誹謗・中傷・デマ 3位 テレワーク等のニューノーマルな働き方を狙った攻撃 NEW
5位 メールやSMS等を使った脅迫・詐欺の手口による金銭要求 4位 サプライチェーンの弱点を悪用した攻撃 4位
3位 クレジットカード情報の不正利用 5位 ビジネスメール詐欺による金銭被害 3位
4位 インターネットバンキングの不正利用 6位 内部不正による情報漏えい 2位
10位 インターネット上のサービスからの個人情報の窃取 7位 予期せぬIT基盤の障害に伴う業務停止 6位
9位 偽警告によるインターネット詐欺 8位 インターネット上のサービスへの不正ログイン 16位
6位 不正アプリによるスマートフォン利用者への被害 9位 不注意による情報漏えい等の被害 7位
8位 インターネット上のサービスへの不正ログイン 10位 脆弱性対策情報の公開に伴う悪用増加 14位

事実上の戦争であって、誰もが巻き込まれており、他人事にはできそうにありません。

戦争を起こさんと脅威をデザインする攻撃者達の、考え方や道具( ~= 攻撃的セキュリティ)を学び理解することで、防衛に役立てることができるでしょう。 *2

参考リンク

概論

脅威モデリング

ニュース記事等

ペネトレーションテスト と Hack The Box

前章でそれっぽい意義を与えたので、技術者にとって魅力的な点を強調していきしょう *3

攻撃的セキュリティの実践的活動のうち、特に代表的な ペネトレーションテスト - Wikipediaペネトレ / Pentest とも)より、

ネットワークに接続されているコンピュータシステムに対し、実際に既知の技術を用いて侵入を試みることで、システムに脆弱性がないかどうかテストする手法

Pentest の独学はかなり難しく、なぜなら

  • 脆弱性のあるシステムを用意するのがやや難しい
  • 現実世界で攻撃をするのは犯罪である

ため。ただ、最近はこのハードルを下げてくれるサービスがさまざま存在します。例えば、

特に Hack The Box (HTB) の Machine と名前のついたカテゴリが、Pentest に近い分野です。

攻撃的セキュリティ と ソフトウェア考古学 の関係

次の点で、 攻撃的セキュリティソフトウェア考古学 はかなり似ています。 *4 「オペレータ」を、それぞれの活動をする主体として、

  • オペレータは、対象のシステムに対し事前知識を(ほとんど)持たない
  • オペレータは、対象システムから効率的に情報を取得して利用したい
  • オペレータは、対象システムを壊したくない

ホワイトハッカーの資格(Certified Ethical Hacker)の母体となる団体の ECCouncil によれば、攻撃の手順は5段階に分けられます。 Learn the 5 Phases of Ethical Hacking and Build a Career in it より、

  1. Reconnaissance (偵察)
  2. Scanning (スキャン)
  3. Gaining Access (アクセスの取得)
  4. Maintaining Access (アクセスの維持)
  5. Clearing Tracks (痕跡の除去)

このうち、1~3 で必要な技術は、遺跡調査に応用できます。

例えば、遺跡調査では、ネットワークスキャンによって対象サーバを発見し、対象サーバに対するポートスキャンによってサービスを特定することがあります。

遺跡調査では痕跡の除去は必ずしも必要ありませんが、下手なことをして二次被害を起こしたくありません。慎重な作業が求められるという点で同じと言えます。

まとめると、

  1. Hack The Box で勉強することで、
  2. 攻撃的セキュリティを学び、調査力がつくことで、
  3. ついでに考古学力を高めて、圧倒的な事業貢献となる

わけですね。こうしてつよつよエンジニアが量産されるのです。

いざ、攻撃的セキュリティに取り組む

もっとハードルを下げる

実は Hack The Box は参加すること自体がちょっと難しいです。ログインページでちょっとした問題を解く必要があります。

さらに、コンテンツは全て英語なため、念能力の素養のない Newbie にはなかなか勧めることができません。

そういった場合は、CpawCTF - Main pageEnvader | Linuxが学べるオンライン学習サイト が オススメできます。

強くなるには

ひとまず、Retired Machine を 公式の walkthrough や IppSec - YouTube の動画を見ながらたくさん解いてみるといいです。それで雰囲気が掴めると思います。

www.youtube.com www.youtube.com

でも、 Active Machine に30時間くらい取り組んでも解けないことは普通にあります。強くなりたければ、人間性を捧げましょう。

f:id:photosynth-inc:20211205193137p:plain
Kaggleを取り掛かるまでにやったこととと、モチベーションの維持のために必要だったこと - にほんごのれんしゅう より引用

なお、僕はまだ人間性を宿しています(Hacker になれてない ..........)。

参考リンク


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com

*1: jmalarcon/markdowntables: markdownTables - Turn HTML table syntax into Markdown を使って変換

*2: なお、懲罰的抑止モデルには攻撃を抑止する効果がない、という議論があります( サイバー空間の安全保障をめぐる課題とアメリカの動向JIIA -日本国際問題研究所-研究活動 など)。こういった知識は、抑止目的ではなく、防衛目的で利用することになります。

*3: 資源の限定された企業活動では、たぶんランチェスター戦略が有効。なお、研究活動に選択と集中を要求するのは間違い。

*4: ただし DoS攻撃や Suicide Hacker を除く

考古学者の心得と、遺跡調査に使える道具

この記事は Calendar for Akerun | Advent Calendar 2021 - Qiita の5日目の記事です。

ご無沙汰しています、ハードウェア開発チームに来たはいいものの、実は低レイヤ開発はあまりしていない daikw - Qiita です。最近は主に考古学者をしていました。

Akerunサービスにおいて、ファームウェアの開発は大きなボリュームを占めるものの一つ、当然重要です。 しかし、作ったそばから陳腐化・レガシー化・遺跡化していくものばかり。

「ドキュメント? そんなものはない

人手があったとしても、これは避けられないのかもしれない。 *1

従って、考古学の力が試されるなぁ、と感じる日々を過ごしています。 今日は記事を書きながら、遺跡調査の道具箱を整理しよう!

言葉の定義

さて、まず智慧を司るトト神の不興を買わないように、未定義語 *2 を減らす努力をします。 誠意を持った祈りは届くのです 🙏 📿

  • 陳腐化: 一般的には、古くなり、当初の価値を減じた状態のこと。*3 本記事で特定のソースコードやシステムに対して使った場合、誰にも読まれなくなり、誰も把握していない状態を指す。
  • 遺跡・ダンジョン: 本記事では、陳腐化した状態の特定のシステム(ネットワーク・サーバ等の集合)を指す。
  • 考古学・遺跡調査: 本記事では、遺跡に対しての活動全般を指す。調査をした上で、何か変更をすることが多い。 実は Software archaeology - Wikipedia という分野があり、伊達や酔狂とも言い切れない。
  • 道具箱: 本記事では、主にコマンドラインツールの集合を指す。ヘッドレスサーバを扱うことが多いため、デスクトップアプリケーションはたまにしか出てこない。
  • トト神: 知恵の神、書記の守護者、時の管理人、楽器の開発者、創造神、または森羅万象を把握しているスーパーエンジニアのこと。 / トート - Wikipedia より。

考え方

考古学者の心得リスト

まず、考古学マインドセットを内面化するための、心得リストを用意します。

今にも崩れそうな遺跡に苦しむ子羊たちを導かんと、各地のトト神による ヒエログリフ解読方法託宣黒魔術 といったコンテンツがインターネット上に存在します。

その中で、1. 特に抽象度が高い または 2. レビュー論文的性質がある ものを心得リストとしました。

心得リストからの教訓

例えば ノーヒントサーバ調査 (Linux編) - Qiita # 調査の心構え より抜粋すると、

  • サーバの今がわかる
    • 「現在サーバがどうなっているか」に答えられるようになる
    • 現状調査の方法 理解が必要
      • サーバスペック / サーバ構成 / リソース状況 / ...
  • サーバの過去がわかる
    • 「過去サーバがどうなっていたか」に答えられるようになる
    • 主に ログの読み方 理解が必要
  • サーバの未来がわかる
    • 「今後サーバがどうなりそうか」に答えられるようになる
    • もちろんここは 推論 が入る

  • 事実と推論を分けて整理する
    • ノーヒントであるがゆえに、当然ファーストステップは推論になるのですが、推論に推論を重ねていくと意味がわからなくなるので、推論のあとは事実確認するようなことが望ましいです
    • そういう意味では一人でガリガリ調べ続けるよりは、別の人も巻き込んでワイワイ(?)やるのがいいですね
  • 調査事故を起こさない
    • たまに障害調査で 二次障害 を引き起こすケースがあります
      • 安易なroot作業や、調査作業によるリソース占有など
    • 「今自分がなにをやろうとしているか?」についてはいつも 気をつけましょう
    • やらかしてしまった場合に備える意味でも ターミナルログ は必ず取りましょう
      • 多分どんなターミナルでもログは取れますが、もし取れないものを使っているとしたらそれは窓から投げ捨てましょう(ポイー

他の心得リストからも、似たような教訓が得られます。概ね、以下の2つに大別されます。

  • 歴史に学べ
  • 記録せよ

歴史に学ぶ

手がかりが本当に全くない、という状況は少なく、探索的調査によって明らかにできます(強い言葉を(ry 。

そもそも遺跡が存在していること自体がヒントになり、歴史は社内の情報源のクロールによって明らかに(きっと)できます。 歴史を知った段階で、ダンジョン化の要因を把握し取り除くことができればより好ましく、価値のある活動に。

資源配分・ITへの無理解 などの構造的問題や、単なるサボり・技術力不足といった小さい問題のこともあります。

記録する

これまで記録がなかったから、ダンジョン化しているわけであって、悲劇を繰り返す必要はありません。 憎しみの連鎖は自分の代で断つ、という覚悟が重要です。

記録し、公開し、騒いで、多くの目に触れさせることで、徐々に良くしていきましょう。 ただし、政治的活動や緻密な戦略が必要なことも、、、

qiita.com zenn.dev

道具

dic.nicovideo.jp

「歴史に学」び、「記録」するために、十分リッチな道具を用意します。各記事からざっくり抜粋し、まとめました。

記載のコマンドは全て、manページをざっくり読み、オプションを含めた概要を掴んでおくことが推奨されます。 といってもdistro間の細かい違いまで抑えるのはやはり困難です。都度、必要な道具を探し、見つけ、学ぶことが肝要そう。

一番いいのを頼む

# ターミナル操作ログを残す。自動で残るようにしておくと便利。
script $file_name

# 外部から、又は外部環境ごと調べる
dig
nslookup
ping
traceroute
nmap
telnet
arp
nc
curl
wget

# 証跡やログ
history
last
journalctl
ls /var/log
less /var/log/messages

# リソース
uptime

cat /proc/cpuinfo
cat /proc/meminfo

dmesg | tail

free -mh
df -h
vmstat 1 5

[h]top

## `sysstat` が必要
mpstat -P ALL 1
pidstat 1
iostat -xz 1
sar -n DEV 1
sar -n TCP,ETCP 1

# ペリフェラル
ls -l /dev

# 固有情報
uname -a
lsb_release
ls /etc/*release
cat /etc/os-release

# プロセス
ps -A o user,command --no-header --sort command | grep -v -e '\s\['
ps aux[f]
pgrep $name
strace -p [-f] $pid [-e trace=$type]

# Cron / Daemon
ls /var/spool/cron/
ls /etc/crontab/
ls /etc/cron.d/
ls /etc/cron.hourly/
ls /etc/cron.daily/
ls /etc/cron.weekly/
ls /etc/cron.monthly/

systemctl list-units --type=service
systemctl list-unit-files --type=service

# ネットワーク
ip a
ip a s $nic
netstat -lntup
ss

# パッケージリスト
dpkg -l
dpkg-query -l
zgrep install /var/log/dpkg.log*

apt list --installed
zgrep Commandline /var/log/apt/history.log.*

comm -23 <(apt-mark showmanual | sort -u) <(gzip -dc /var/log/installer/initial-status.gz | sed -n 's/^Package: //p' | sort -u)
### ref: https://askubuntu.com/questions/2389/how-to-list-manually-installed-packages

rpm -qa --last
yum list installed

snap list
flatpak list

# 特徴的なファイルを見つける・中身を探る
du -m / --max-depth=3 --exclude="/proc*" | sort -k1 -n -r
find /etc -maxdepth 3 -type f -printf "%p %TY-%Tm-%Td\n" | sort -k2 -r
find /home/pi/ -type f -exec du -sm {} \; | sort -nrk1 | head
find /var/log/ -type f -exec du -sm {} \; | sort -nrk1 | head

file $file_name
stat $file_name

参考リンク


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com

*1: そもそも全てが IaC化 / 仕組み化されていれば、こんなことを考える必要はない...。 弱者の論理ではある。

*2: Undefined (mathematics) - Wikipedia

*3: 陳腐化とは - コトバンク

専用アプリはもう要らない?LINE BotからAkerunを操作する(その3)

この記事は Akerun Advent Calendar 2021 - Qiita の2日目の記事です。

WebエンジニアのBunです。主にiOSアプリの開発を担当しています。

2019年と2020のAdvent Calendarでは、「LINE BotからAkerunを操作する(その1)と(その2)」を書かせていただきました。 この2回でご紹介したAkerun APIの基本機能(施錠解錠)とLINE Bot(基本機能、LINEログイン、グループ機能)を連携すれば、十分使えると思いますが、今年の3回目では、Akerun APIから取得した事業所/Akerun/合鍵/ユーザー情報をLINE Botの少しリッチな表示機能のカルーセル表示についてご紹介します。

akerun.hateblo.jp

akerun.hateblo.jp

事前準備

LINE Botの設定とAkerun APIアクセストークンの生成

その1を参考にLINE Botを作成し、Botと友達になります。

アクセストークンについては、弊社のAkerun Developersをご参照ください。

developers.akerun.com

Akerun API

以下のAkerun APIを使って、事業所一覧、事業所詳細、Akerun一覧、Akerun詳細、合鍵一覧、合鍵詳細、ユーザー一覧、ユーザー詳細を取得します。

Akerun APIの詳細についてAkerun Developersをご参照ください。

事業所一覧
GET https://api.akerun.com/v3/organizations

事業所詳細
GET https://api.akerun.com/v3/organizations/{ORGANIZATION_ID}

Akerun一覧
GET https://api.akerun.com/v3/organizations/{ORGANIZATION_ID}/akeruns

Akerun詳細
GET https://api.akerun.com/v3/organizations/{ORGANIZATION_ID}/akeruns/{AKERUN_ID}

合鍵一覧
GET https://api.akerun.com/v3/organizations/{ORGANIZATION_ID}/keys

合鍵詳細
GET https://api.akerun.com/v3/organizations/{ORGANIZATION_ID}/keys/{KEY_ID}

ユーザー一覧
GET https://api.akerun.com/v3/organizations/{ORGANIZATION_ID}/users

ユーザー詳細
GET https://api.akerun.com/v3/organizations/{ORGANIZATION_ID}/users/{USER_ID}

pythonのコードは下記になります。

def akerun_bot_get_organizations(akerun_token):
  ak_url = 'https://api.akerun.com/v3/organizations'
  ak_headers = {'Authorization': 'Bearer ' + akerun_token}
  resp_data = akerun_bot_api_get(ak_url, ak_headers)
  return resp_data["organizations"] if resp_data is not None else []

def akerun_bot_get_organization_info(akerun_token, org_id):
  ak_url = 'https://api.akerun.com/v3/organizations/{}'.format(org_id)
  ak_headers = {'Authorization': 'Bearer ' + akerun_token}
  resp_data = akerun_bot_api_get(ak_url, ak_headers)
  return resp_data["organization"] if resp_data is not None else None

def akerun_bot_get_akeruns(akerun_token, org_id):
  ak_url = 'https://api.akerun.com/v3/organizations/{}/akeruns'.format(org_id)
  ak_headers = {'Authorization': 'Bearer ' + akerun_token}
  resp_data = akerun_bot_api_get(ak_url, ak_headers)
  return resp_data["akeruns"] if resp_data is not None else []

def akerun_bot_get_akerun_info(akerun_token, org_id, ak_id):
  ak_url = 'https://api.akerun.com/v3/organizations/{}/akeruns/{}'.format(org_id, ak_id)
  ak_headers = {'Authorization': 'Bearer ' + akerun_token}
  resp_data = akerun_bot_api_get(ak_url, ak_headers)
  return resp_data["akerun"] if resp_data is not None else None

def akerun_bot_get_keys(akerun_token, org_id):
  ak_url = 'https://api.akerun.com/v3/organizations/{}/keys'.format(org_id)
  ak_headers = {'Authorization': 'Bearer ' + akerun_token}
  resp_data = akerun_bot_api_get(ak_url, ak_headers)
  return resp_data["keys"] if resp_data is not None else []

def akerun_bot_get_key_info(akerun_token, org_id, key_id):
  ak_url = 'https://api.akerun.com/v3/organizations/{}/keys/{}'.format(org_id, key_id)
  ak_headers = {'Authorization': 'Bearer ' + akerun_token}
  resp_data = akerun_bot_api_get(ak_url, ak_headers)
  return resp_data["key"] if resp_data is not None else None

def akerun_bot_get_users(akerun_token, org_id):
  ak_url = 'https://api.akerun.com/v3/organizations/{}/users'.format(org_id)
  ak_headers = {'Authorization': 'Bearer ' + akerun_token}
  resp_data = akerun_bot_api_get(ak_url, ak_headers)
  return resp_data["users"] if resp_data is not None else []

def akerun_bot_get_user_info(akerun_token, org_id, user_id):
  ak_url = 'https://api.akerun.com/v3/organizations/{}/users/{}'.format(org_id, user_id)
  ak_headers = {'Authorization': 'Bearer ' + akerun_token}
  resp_data = akerun_bot_api_get(ak_url, ak_headers)
  return resp_data["user"] if resp_data is not None else None

def akerun_bot_api_get(url, headers):
  obj_session = Session()
  obj_request = Request("GET",
              url,
              headers=headers
              )
  obj_prepped = obj_session.prepare_request(obj_request)
  obj_response = obj_session.send(obj_prepped,
                  verify=True,
                  timeout=60
                  )
  
  print('status_code:' + str(obj_response.status_code))
  print('obj_response.text:' + obj_response.text)
  if obj_response.status_code == 200:
    response_dict = json.loads(obj_response.text)
    print(response_dict)
    return response_dict

LINE Bot(Messaging API)のテンプレートメッセージ

今回はテンプレートメッセージのカルーセルと画像カルーセルを使って、事業所とAkerunを少しリッチに表示します。

developers.line.biz

事業所一覧(カルーセルテンプレート)

Akerunのユーザーは複数の事業所に所属できるので、事業所一覧を横スクロール可能なカルーセルテンプレートで表示します。

ルーセルテンプレートは複数のカラムを表示するテンプレートです。カラムは横にスクロールして順番に表示できます。

複数のActionも設定可能なので、事業所のAkerun一覧、合鍵一覧、ユーザー一覧を取得するActionも追加可能です。

事業所一覧取得メニューをTapしたら、Akerun APIから事業所一覧と詳細を取得し、カルーセルテンプレートで返すようにしています。

※今回は適当に「organizations」文字列で代替していますが、その1を参考にリッチメニューにメニューを追加すれば良いと思います。

# TextMessageとして「organizations」が通知される
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
  reply_msgs = []
  if event.message.type == 'text':
      cmd = event.message.text
      # 環境変数から取得したTokenを使用
      akerun_token = akerun_temp_token
      if cmd == 'organizations':
        organizations = akerun_bot_get_organizations(akerun_token)
        org_action_columns = []
        for org in organizations:
          org_id = org['id']
          if org_info is not None:
            org_name = org_info['name']
            org_id = org_info['id']
            org_action_columns.append(
              CarouselColumn(
                title = org_name,
                text = u'事業所の詳細情報を取得',
                default_action = PostbackTemplateAction(
                  label=u'Akerun一覧を取得',
                  data=json.dumps({'org_id': org_id})
                ),
                actions = [
                  PostbackTemplateAction(
                    label=u'Akerun一覧を取得',
                    data=json.dumps({'cmd': 'get_akeruns', 'org_id': org_id})
                  ),
                  PostbackTemplateAction(
                    label=u'鍵一覧を取得',
                    data=json.dumps({'cmd': 'get_keys', 'org_id': org_id})
                  ),
                  PostbackTemplateAction(
                    label=u'ユーザー一覧を取得',
                    data=json.dumps({'cmd': 'get_users', 'org_id': org_id})
                  ),
                ]
              )
            )

            # 最大10個
            if len(akerun_action_columns) >= 10:
              break

        if len(org_action_columns) > 0:
          reply_msgs.append(
            TemplateSendMessage(
              alt_text=u'事業所一覧',
              template=CarouselTemplate(
                columns = org_action_columns
              )
            )
          )
        else:
          reply_msgs.append(
            TextSendMessage(text = u'所属している事業所がありません。')
          )

      if len(reply_msgs) > 0:
        line_bot_api.reply_message(event.reply_token, reply_msgs)

Akerun一覧(画像カルーセルテンプレート)

Akerun一覧は横スクロール可能な画像カルーセルテンプレートで表示します。

画像カルーセルテンプレートは複数の画像を表示するテンプレートです。画像は横にスクロールして順番に表示できます。

事業所一覧のカルーセルのActionではPostbackTemplateActionを使ったので、Akerun一覧をTapした時のイベントはPostbackEventで拾います。

@handler.add(PostbackEvent)
def handle_postback_event(event):
  uid = get_id(event.source)
  data = json.loads(event.postback.data)
  cmd = data.get('cmd')
  reply_msgs = []

  if cmd == 'get_akeruns':
    akerun_token = akerun_temp_token
    org_id = data.get('org_id')
    akeruns = akerun_bot_get_akeruns(akerun_token, org_id)
    akerun_action_columns = []
    for ak in akeruns:
      ak_id = ak['id']
      ak_name = ak['name']
      ak_imgage = ak['image_url']
      ak_action = PostbackTemplateAction(
        label=org_name,
        data=json.dumps({'cmd': 'select_akerun', 'value': 'token={}&org_id={}&ak_id'.format(akerun_token, org_id, ak_id)})
      )
      akerun_action_columns.append(
        ImageCarouselColumn(
          image_url = ak_imgage,
          action = PostbackTemplateAction(
              # 12文字超える
              # label=ak_name + u'を解錠',
              label=ak_id + u'を解錠',
              data=json.dumps({'cmd': 'unlock', 'org_id': org_id, 'ak_id': ak_id})
          )
        )
      )

      # 最大10個
      if len(akerun_action_columns) >= 10:
        break
    
    if len(akerun_action_columns) > 0:
      reply_msgs.append(
        TemplateSendMessage(
          alt_text=u'Akerun一覧',
          template=ImageCarouselTemplate(
            columns=akerun_action_columns
          )
        )
      )
    else:
      reply_msgs.append(
        TextSendMessage(text = u'使用可能なAkerunがありません。')
      )
    
    if len(reply_msgs) > 0:
      line_bot_api.reply_message(event.reply_token, reply_msgs)

ただ、画像カルーセルテンプレートはカルーセルテンプレートと違って、画像が全領域に表示されて、Actionも一つしか設定できない(施錠か解錠しかできない)ので、カルーセルテンプレートの方が良さそうです。

上記のImageCarouselTemplateの部分をCarouselTemplateに変更します。

akerun_action_columns.append(
  CarouselColumn(
    title = ak_name,
    text = u'施錠解錠',
    thumbnail_image_url = ak_imgage,
    default_action = PostbackTemplateAction(
      label=u'解錠',
      data=json.dumps({'cmd': 'unlock', 'org_id': org_id, 'ak_id': ak_id})
    ),
    actions = [
      PostbackTemplateAction(
        label=u'施錠',
        data=json.dumps({'cmd': 'lock', 'org_id': org_id, 'ak_id': ak_id})
      ),
      PostbackTemplateAction(
        label=u'解錠',
        data=json.dumps({'cmd': 'unlock', 'org_id': org_id, 'ak_id': ak_id})
      )
    ]
  )
)


reply_msgs.append(
  TemplateSendMessage(
    alt_text=u'Akerun一覧',
    # template=ImageCarouselTemplate(
    #   columns=akerun_action_columns
    # )
    template=CarouselTemplate(
      columns = akerun_action_columns
    )
  )
)

施錠解錠

Akerun一覧のカルーセルのActionもPostbackTemplateActionを使ったので、施錠解錠をTapした時のイベントはPostbackEventで拾います。

施錠解錠の詳細について、その1とその2をご参照ください。

@handler.add(PostbackEvent)
def handle_postback_event(event):
  uid = get_id(event.source)
  data = json.loads(event.postback.data)
  cmd = data.get('cmd')
  reply_msgs = []

  if cmd == 'get_akeruns':
    # akerun一覧
  elif cmd == 'unlock':
    profile = line_bot_api.get_profile(event.source.user_id)
    name = profile.display_name
    push_msgs = []
    org_id = data.get('org_id')
    ak_id = data.get('ak_id')
    job_id = akerun_bot_unlock(akerun_temp_token, org_id, ak_id)
    reply_msgs.append(TextSendMessage(text=u'解錠'))
    push_msgs.append(TextSendMessage(text = u'{}さんが施錠しました'.format(name)))
    # todo:pooling -> push
    if len(reply_msgs) > 0:
      line_bot_api.reply_message(event.reply_token, reply_msgs)
    if len(push_msgs) > 0:
      line_bot_api.push_message(test_group_id, push_msgs)

合鍵一覧とユーザー一覧

合鍵一覧とユーザー一覧取得APIを使えば、後はAkerun一覧と同じ方法で表示できるので、詳細については割愛します。

まとめ

  • LINE Messaging APIではテキストだけではなく、カルーセルテンプレートのようなもっとリッチな表示が可能で、対応方法も簡単です。

  • シンプルに画像をTapした時のActionを実現する場合は画像カルーセルテンプレート、画像も表示しつつ複数のActionを実現する場合はカルーセルテンプレートを使えば良さそうです。

  • テンプレートMessage以外にも、画像、動画、音声、位置情報、イメージマップMessageとLayoutを自由にカスタマイズ可能なFlex Messageもあります。

  • Akerun APIには合鍵発行、ユーザー登録、ICカード登録、施錠解錠履歴取得などの機能もあるので、上記Messageを使ってみたいと思います。


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com

便利な解錠方法-AppClip解錠

この記事は Akerun Advent Calendar 2021 - Qiita の1日目の記事です。

WebエンジニアのBunです。主にiOSアプリの開発を担当しています。

日々いろんな解錠方法について考えています。特にiOSアプリで使えるいろんな便利な機能を使った解錠方法とその実装手順をまとめます。 今回はApp Clipでの解錠について説明します。

App Clipとは

iOS14から使えるApp Clipはアプリの一部分で、アプリの一部機能やコンテンツを必要な時に簡単に見つけて、すばやくアクセスし、体験することができます。

※弊社のAkerunの本体、あるいは近いところに専用QRコード/NFCタグを配置すれば、事前にAkerunアプリをインストールしなくても、1タッチ/1タップでの解錠が実現可能。

以下の特徴があります。

  • 高速で軽量(10MB以下)のため、すばやく起動可能。わずか数秒でダウンロード->インストール->使用まで可能
  • 通常の完全版アプリと同じ方法で開発可能(一部使用可能なiOS SDK制限あり)
  • 同じアプリに複数のApp Clipを作成可能
  • 完全版アプリのダウンロードを促すことが可能
  • 既に完全版アプリがインストールされている場合、完全版アプリが起動される
  • Apple Pay」、「Appleでサインイン」との相性が良い(ユーザー登録、合鍵発行など、良いUX実現可能)
  • App Clip専用の位置情報確認用APIを使用して、App Clipコード/NFCタグ/QRコードが所定場所で使用されているか確認可能
  • App Clipを見つけて、起動する方法は多数ある
    • Apple製のURL/NFCタグが埋め込まれているApp Clipコードをタップしたり、カメラでスキャンしてApp Clipを起動(以下のNFCタグ、QRコードが合体されている)
    • 通常のNFCタグにiPhoneをかざして起動。ロック画面からでも起動可能
    • QRコードを作成し、カメラでスキャンして起動
    • Safari App Banner(Smart App Banner)を使って、WebページのBannerをタップして起動
    • iMessage、Mailで共有されたLinkをタップして起動
    • マップ上のマーカーをタップして起動
    • 「最近追加した項目」カテゴリに表示されるので、そこから再起動
    • App Clip起動から8時間以内であれば、新しいPush通知から再起動可能

App Clipから解錠

URLが埋め込まれたQRコードをカメラでスキャンすると、下記にようにApp Clipがすぐ起動されます。ボタン(Labelはカスタタイズ可能)をタップすると、指定画面に遷移され、解錠処理が実行されます。

App Clipの実装方法

App Clipの追加

  • 既存アプリにApp ClipのTargetを新規追加

  • 「Next」画面でProject名(例えば、既存アプリ名+AppClip)を入力し、UIKit AppかSwiftUI Appを選択

App Clip起動(XXボタンをタップ)した時の処理

App Clipから起動された場合、NSUserActivityがイベントに渡されるので、NSUserActivityからURLを取得、解析後、指定画面に遷移し、処理時実行します。 ※URL例:https://ドメイン/unlock/door?id=1001

  • UIKit Appの場合(Universal Linksと同じ方法)
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
        guard let url = userActivity.webpageURL, let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return false }
        // componentsからURLパスを解析、Queryなどを抽出し、関連画面に遷移、処理時実行
    }
}
  • SwiftUI Appの場合、Universal Linksと異なって、onOpenURLではなくonContinueUserActivityが呼ばれるので、表示したい画面でイベントを拾う
struct ContentView: View {
    var body: some View {
        NavigationView {
            ...
        }
        .onContinueUserActivity(NSUserActivityTypeBrowsingWeb, perform: handleUserActivity(_:))
    }
}

extension ContentView {
    func handleUserActivity(_ userActivity: NSUserActivity) {
        guard
            let incomingURL = userActivity.webpageURL,
            let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
            let queryItems = components.queryItems
        else {
            return
        }
        
        if let id = queryItems.first(where: { $0.name == "id" })?.value {
            // idの鍵/ドアの解錠処理を行う
        }
    }
}
  • UIKitのSceneDelegateライフサイクルの場合、Univeral Linksと同じ方法で対応
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    // Univeral Links / AppClip
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
        guard
            let incomingURL = userActivity.webpageURL,
            let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
            let queryItems = components.queryItems
        else {
            return
        }
        
        if let id = queryItems.first(where: { $0.name == "id" })?.value {
            // idの鍵/ドアの解錠処理を行う
        }
    }
}

解錠処理の実装

Akerun公開APIで合鍵発行、解錠が可能です。 具体的な実装は割愛します。弊社のAkerun Developersと下記の関連記事をご参照ください。

developers.akerun.com

akerun.hateblo.jp

akerun.hateblo.jp

アプリ本体の対応

既にアプリ本体がインストールされている場合、App Clipではなくアプリ本体が起動されるので、アプリ本体にもUniveral Linksで同じ対応を入れる必要があります。

ドメインの登録

Universal Linksと同じくサーバーのドメインを登録する必要があります。

  • json形式(.json拡張子不要)のapple-app-site-associationファイルを.well-knownに配置し、直接Getできるようにする(Redirectはダメ)

  • apple-app-site-associationファイルにApp ClipアプリのBundle Identifyを追加

{
    "appclips": {
        "apps": [
            "teamId.appId.Clip",
            "teamId.appId.Stg.Clip" ←Staging/Debug版アプリがあれば
        ]
    },
    "applinks": { ←アプリ本体Universal Links用
        "apps": [],
        "details": [
            {
                "appID": "teamId.appId",
                "paths": [
                    "unlock/*", ←解錠
                    "receive/*" ←合鍵受取
                ]
            },
            {
                "appID": "teamId.appId", ←Staging/Debug版アプリのID
                "paths": [
                    "unlock/*", 
                    "receive/*"
                ]
            }
        ]
    }
}
  • アプリ本体とApp ClipアプリのCapabilitiesにAssociated Domainsを追加し、ドメインを設定(アプリ本体にUniversal Links用のドメインも追加)

AppStoreConnectでの設定

App Clipが起動された時の画面表示を設定する場合、アプリをArchiveし、AppStroeConnectにアップロードしてから設定を行います。

  • 基本設定:背景画像、サブタイトル、アクション(ボタンラベル)を設定

  • NFCタグ、QRコードに埋め込むURLを作成したり、App Clipの詳細設定をおこな場合、「高度な体験を編集」をクリックする

apple-app-site-associationを設定すると、エラーは消える。

App ClipのDebug

指定URLから正常にApp Clipが起動されるか以下の方法でDebug可能です。URLのLinkを直接Tapして起動、あるいはNFCタグ/QRコードからの起動が可能です。

Schemeの設定

App ClipのScheme(Run->Arguments->Environment Variables)にテストURLを設定します。

iPhoneでLocal Experiencesを登録

iPhoneの設定->Developer->Local ExperiencesにURLごとの設定を追加します。複数のURLの登録ができ、以下の項目が設定可能です。

  • URL Prefix:Schemeに設定したURLを設定
  • Bundle ID:App ClipのBundle IDを設定
  • タイトル、サブタイトル:任意の文字列
  • アクション(ボタンラベル):一覧から選択
  • 背景画像:任意のPNG/JPEG画像。1800x1200ピクセルで、できるだけ小さいサイズの画像の方が良い

Debugを実施

以下の手順でDebugを実施します。

  • XcodeからApp Clipを起動
  • 上記のURLをSafariで開く。QRコードNFCタグからも起動可能
    • URLのQRコードを生成し、カメラからスキャン
    • 市販のNFCタグを購入し、URLを書き込む
  • URLのLinkをTap(あるいは、QRコードをスキャン/iPhoneNFCタグをタップ)すると、App Clipが起動される

その他

解錠だけではなく、「Appleでサインイン」を使えば、ユーザー登録、合鍵登録も簡単にできそうです。

  • ユーザー登録、合鍵登録用URLを発行する
  • ユーザーにURLを共有する
    • 直接Linkを共有
    • QRコードを共有、あるいは、印刷して指定場所に配置
    • NFCタグにURLを書き込んで、指定場所に配置
  • ユーザーはLinkをTap/QRコードをスキャン/iPhoneNFCタグをTapすることでApp Clipアプリを素早く起動(事前にアプリのインストールは不要)
  • Appleでサインイン」でサインインし、ユーザーを登録/合鍵を受け取る
    • ユーザー登録する場合、ユーザーのApple IDを使って新規ユーザー登録を行う
      • iPhoneで生体認証(顔認証、指紋認証
      • 登録に必要な情報(メールアドレス、Tokenなど)をサーバーに送信し、ユーザー登録を行う
    • 合鍵受け取る場合、「Appleでサインイン」でサインインし、合鍵を受け取る
      • まだユーザー登録されていない場合、ユーザー登録->合鍵を受け取る
      • 登録済みの場合、Apple IDでサインインし、合鍵を受け取る

まとめ

App Clipはユーザーにとても優れたUXを提供できる機能です。

URLベースになっているので、実装上Universal Linksとはそれほど差異がありません。また、設定もシンプルなので、どんどん活用して行きたいですね。

次回は、Widgetについて書こうと思います。


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com

便利な解錠方法-NSUserActivity-SiriShortcuts解錠

WebエンジニアのBunです。主にiOSアプリの開発を担当しています。

日々いろんな解錠方法について考えています。特にiOSアプリで使えるいろんな便利な機能を使った解錠方法とその実装手順をまとめます。 今回はNSUserActivityを利用したSiri Shortcutsでの解錠について説明します。

NSUserActivityとは

抽象的な言い方だと、NSUserActivityは「ある時点でのアプリの状態」を表現します。

ユーザーがアプリを使って、コンテンツの表示、ドキュメントの編集、Webページの表示、ビデオの視聴などを行った時の状態(関連情報)をNSUserActivityのオブジェクトとして保存すれば、再度アプリが起動された時、上記の状態から復元することができます。

一例として、以下が可能です。詳細について、今後順次紹介します。

  • Shortcutsアプリに登録したShortcutのActionを実行した時、指定画面に遷移させたり、機能を実現可能
  • 音声でアプリを起動し、指定画面に遷移させたり、機能を実現可能
  • Hand-off/Hand-freeでMacで途中までの作業をそのままシームレスでiPhoneで作業再開可能
  • 端末内での検索可能
  • Webと連携すれば、Global検索も可能
  • iOS15からのQuickNoteでも使われている(まだ使ったことない)

Siri Shortcutsの使い方(解錠方法)

  • Siri Shortcutsに登録すれば、使用頻度が上がる時、Siriからの提案にShortcut(解錠)が表示され、1Tapで解錠可能

※Siriからの提案:Home画面を下にSwipe、ロック画面で左にSwipeした時表示される検索窓(Spotlight)の一番上位に表示される

  • Siriを呼び出して、音声から解錠可能。登録済みの音声フレーズ「あけるん」を言えば、アプリが起動され、指定画面に遷移し、対象ドアを解錠できる。

※端末によるかも知れないが、 Home画面(生体認証後のLock画面)でサイドボタンを2回ClickでSiriを呼び出せる。もちろん「Hey Siri」でもOK

  • オートメーションを作れば、「xxx」の時「解錠」することも可能。最後に説明する。

Siri Shortcutsの実装方法

Siri Shortcutsの登録

Siri Shortcutsの登録方法には、NSUserActivityを利用した方法とIntentsを利用する方法がありますが、今回は前者について説明します。Intentsについてはまた別の機会でご紹介します。

NSUserActivityTypesを定義

Info.plistのNSUserActivityTypesにNSUserActivity作成に必要なIDを定義します。 一意であれば何でも良いですが、アプリのbundleId+機能ごとの文字列で良いと思います。

<key>NSUserActivityTypes</key>
<array>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER).unlock-door</string>
</array>

アクションを行うNSUserActivityオブジェクトを生成

  • オブジェクト生成
class SKUserActivity {
    enum ActivityType: String {
        case doorUnlock = "unlock-door"
        case keyShare = "share-key"
        
        var id: String {
            // TODO: add team id (info.plist)
            return (Bundle.main.bundleIdentifier ?? "") + "." + self.rawValue
        }
    }
    class func create(_ type: ActivityType, _ title: String, _ phrase: String? = nil) -> NSUserActivity {
        // Info.plistに登録したID
        let userActivity = NSUserActivity(activityType: type.id)
        userActivity.persistentIdentifier = type.id
        userActivity.title = title
        // 音声フレーズ(コマンド)。Siri Shortcutsに登録時変更可能
        userActivity.suggestedInvocationPhrase = phrase
        // Siriからの提案を可能にする
        userActivity.isEligibleForPrediction = true
        // 検索可能にする
        userActivity.isEligibleForSearch = true
        return userActivity
    }
}
  • Spotlight検索結果に表示する場合
let attributeSet = CSSearchableItemAttributeSet(contentType: .image)
// 実際userActivity.titleが表示され、このtitleは表示されない
attributeSet.title = "Test"
attributeSet.contentDescription = "Unlock"
userActivity.contentAttributeSet = attributeSet

アクションで開く画面のUIViewControllerにオブジェクトを設定

NSUserActivityのオブジェクトを画面(UIViewController)に紐づけることで、Shortcutsアプリに表示され、登録可能になります。(一度アプリを起動し、該当画面に遷移する必要があるようです)

override func viewDidLoad() {
    super.viewDidLoad()
    self.userActivity = self.siriUserActivity
}

Siri Shortcutsボタンを追加

ユーザーが簡単にSiri Shortcutsを登録できるように、画面に専用ボタンを配置することも可能です。

UIKitの場合、上記のUIViewControllerにINUIAddVoiceShortcutButtonを配置すれば良いですが、SwiftUIの場合、INUIAddVoiceShortcutButtonそのまま使えないので、UIViewControllerRepresentableを使って、UIKit->SwiftUIパーツに変換する必要があります。

  • Siriボタンのパーツを作成
import Foundation
import SwiftUI
import Intents

struct SiriButton: UIViewControllerRepresentable {
    private let userActivity: NSUserActivity
    
    init(userActivity: NSUserActivity) {
        self.userActivity = userActivity
    }
    
    func makeUIViewController(context: Context) -> some UIViewController {
        return SiriShortcutViewController(userActivity: self.userActivity)
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        
    }
}
  • SiriShortcutViewControllerを作成し、INUIAddVoiceShortcutButtonを配置する
import Foundation
import SwiftUI
import IntentsUI

class SiriShortcutViewController: UIViewController {
    private let siriUserActivity: NSUserActivity
    
    init(userActivity: NSUserActivity) {
        self.siriUserActivity = userActivity
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.userActivity = self.siriUserActivity
        // これはなくても良い?(詳細不明)
        self.userActivity?.becomeCurrent()
        
        let siriButton = INUIAddVoiceShortcutButton(style: .blackOutline)
        siriButton.shortcut = INShortcut(userActivity: self.siriUserActivity)
        siriButton.delegate = self
        siriButton.translatesAutoresizingMaskIntoConstraints = false
        
        self.view.addSubview(siriButton)
        self.view.centerXAnchor.constraint(equalTo: siriButton.centerXAnchor).isActive = true
        self.view.centerYAnchor.constraint(equalTo: siriButton.centerYAnchor).isActive = true
    }
}
  • SwiftUIの画面にSiriボタンを配置
struct TestView: View {
    var body: some View {
        VStack() {
            SiriButton(userActivity: SKUserActivity.create(.doorUnlock, "unlock", "あけるん"))
        }
    }
}

SiriボタンでSiri Shortcutsに追加する時の処理

Siri Shortcutsに新規追加、編集、削除時の画面表示するための最小限の実装になります。

extension SiriShortcutViewController: INUIAddVoiceShortcutButtonDelegate {
    func present(_ addVoiceShortcutViewController: INUIAddVoiceShortcutViewController, for addVoiceShortcutButton: INUIAddVoiceShortcutButton) {
        addVoiceShortcutViewController.delegate = self
//        addVoiceShortcutViewController.modalPresentationStyle = .formSheet
        // Shortcut画面を閉じる時の動作検知
        addVoiceShortcutViewController.presentationController?.delegate = self
        
        self.present(addVoiceShortcutViewController, animated: true, completion: nil)
    }
    
    func present(_ editVoiceShortcutViewController: INUIEditVoiceShortcutViewController, for addVoiceShortcutButton: INUIAddVoiceShortcutButton) {
        editVoiceShortcutViewController.delegate = self
        // Shortcut画面を閉じる時の動作検知
        editVoiceShortcutViewController.presentationController?.delegate = self
        
        self.present(editVoiceShortcutViewController, animated: true, completion: nil)
    }
}

extension SiriShortcutViewController: INUIAddVoiceShortcutViewControllerDelegate {
    func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?) {
        controller.dismiss(animated: true, completion: nil)
    }
    
    func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController) {
        controller.dismiss(animated: true, completion: nil)
    }
}

extension SiriShortcutViewController: INUIEditVoiceShortcutViewControllerDelegate {
    func editVoiceShortcutViewController(_ controller: INUIEditVoiceShortcutViewController, didUpdate voiceShortcut: INVoiceShortcut?, error: Error?) {
        controller.dismiss(animated: true, completion: nil)
    }
    
    func editVoiceShortcutViewController(_ controller: INUIEditVoiceShortcutViewController, didDeleteVoiceShortcutWithIdentifier deletedVoiceShortcutIdentifier: UUID) {
        controller.dismiss(animated: true, completion: nil)
    }
    
    func editVoiceShortcutViewControllerDidCancel(_ controller: INUIEditVoiceShortcutViewController) {
        controller.dismiss(animated: true, completion: nil)
    }
}

extension SiriShortcutViewController: UIAdaptivePresentationControllerDelegate {
    func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
        // Shortcut will be closed
    }
    
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        // Shortcut was closed
    }
}

Siriに追加したら、Shortcutsアプリにも表示されます。

直接Shortcutsアプリから登録する場合、ある程度アプリを使わないと表示されないようです。

下記のAppをTapし、表示されるアプリ一覧には表示されません。

Siri Shortcutsから解錠

Shortcut、Siri音声コマンド、Spotlight検索窓のSiriからの提案に表示されているShortcutなどから1Tapでアプリを起動し、解錠することが可能です。 下記でアクションをハンドリングします。

  • UIKitアプリの場合、以下のDelegate関数を実装
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    guard let userInfo = userActivity.userInfo else {
        return false
    }
    guard userActivity.activityType == SKUserActivity.ActivityType.doorUnlock.id else {
        return false
    }
    // userInfoから鍵/ドアのIDを取得し、関連画面に遷移し、解錠処理を行う
    return true
}
  • SwiftUIアプリの場合、表示する画面で以下を実装
struct ContentView: View {
    var body: some View {
        NavigationView {
            ...
        }
        .onContinueUserActivity(SKUserActivity.ActivityType.doorUnlock.id, perform: handleShortcut(_:))
    }
}

extension ContentView {
    func handleShortcut(_ userActivity: NSUserActivity) {
        if let userInfo = userActivity.userInfo {
            // userInfoから鍵/ドアのIDを取得し、解錠処理を行う
            print("userInfo: \(userInfo)")
        }
    }

解錠処理の実装

Akerun公開APIで合鍵発行、解錠が可能です。 具体的な実装は割愛します。弊社のAkerun Developersと下記の関連記事をご参照ください。

developers.akerun.com

akerun.hateblo.jp

akerun.hateblo.jp

Siri Shortcutsを使った便利機能(拡張版解錠方法)

Shortcutsアプリからオートメーションを作成すれば、色んな方法で解錠することができます。

iPhone背面タップして解錠

iOS設定->アクセシビリティ->タッチ->背面タップからアプリのShortcutを登録します。

f:id:photosynth-inc:20210906214945g:plain

QRコードで解錠

ShortcutsアプリからQRスキャンしてアプリを起動するShortcutを作成し、Home画面に追加します。

  • ギャラリーからマイショートカットにQRコードスキャンを追加

  • 既存アクションを削除し、自作アプリを登録

f:id:photosynth-inc:20210906215027g:plain

NFCタグで解錠

新規オートメーションを作成し、NFCスキャンをトリガーに、アプリをアクションに登録します。

f:id:photosynth-inc:20210906215326g:plain

まとめ

NSUserActivityではいろんなことができます。Webと連携することも可能です(別の機会で書く予定)。

その一つのSiri Shortcutsを実装すれば、iOS純正アプリShortcutsを使って無限な便利な機能を提供できそうです。

次回は、Widget、AppClipについて書こうと思います。


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com