Rakuten miniを3000円で買って、みまもり端末にする
つくったもの
聞いた話だと、世の中には「みまもり端末」とかいって月額いくらとかかかるサービスもあるようだ。
— Yusuke Iwaki (@yi01imagination) 2023年5月9日
でも、rakuten miniを中古で2000円ちょいで手に入れたらAndroid Management APIをちょちょいとやって実現できるよね?
実際買ってやってみたけど、これで十分みまもりできそうよ? pic.twitter.com/4RKqkxaCOP
- 学校にいるであろう時間帯は、アプリが何も使えない時計状態
- でも親が本当に連絡を取りたいときに、電話はかかる(時計状態でも、着信画面は出る)
- 動作確認してないが、たぶん地震速報とかそれ系もくる
- スマホ使っていいよって時間帯は、限られたアプリだけ起動できる。
これなら「スマートフォン持ち込みなんて許さん、けど、みまもり端末に限って持ち込んでいいぞ」てきな学校を攻略できるんでは?そういう需要ありそうでは??
とか妄想してみたのであった。
つくりかた
- 楽天ミニの中古デバイス
- eSIM
- 特定のアプリだけを表示するランチャーアプリ(自作する)
- ManagedConfigurationを使って「お家モード」「学校モード」の切り替えができるように作るのがミソ
があればできる。
ランチャーアプリを作る
ランチャーと言っても、今回のユースケースでは任意のアプリを起動できる必要はない。
固定でアプリアイコンを配置してしまって、そのアイコンが押されたら特定のアプリ決め打ちでIntentを投げるだけのウルトラ適当実装で十分だ。
あとは、「お家モード」「学校モード」の2つのFragmentが切り替わるだけの1Activityのアプリを作ればいい。
ふつうにアプリを作る上ではあまり馴染みがないであろうRestrictionsManagerという機能を使い、MDMから流し込むことができる設定を定義・設定値の取得を行う。
Android Management APIでポリシーを作る
今回のアプリはAndroidデバイス管理機能のKIOSKモードで動かす。(POSレジアプリとかでよく使われてるやつ)
ただ、大抵のMDMサービスを契約すると手間もお金もかかるので、簡易的なMDMサービスを自分で作ってしまうのがいいだろう。作り方は去年のアドベントカレンダーに結構細かく書いている。興味ある人は読んでくれ。
で、MDMを作ってどういうポリシーを用意すればいいかというと、こんな感じ。「お家モード用のポリシー」「学校モード用のポリシー」の2つを用意し、お家モードで起動したいアプリは両方のポリシーにFORCE_INSTALLED指定で入れておく。
先の手順で作ったランチャーアプリはprivate appsとしてAndroid Management APIに登録し、以下のようにManaged Configurationを指定してインストール+KIOSKモードにする。
「お家モード用のポリシー」↓
"applications": [ { "packageName": "com.google.android.apps.maps", "installType": "FORCE_INSTALLED" }, { "packageName": "com.google.android.apps.tachyon", "installType": "FORCE_INSTALLED" }, { "packageName": "com.android.chrome", "installType": "FORCE_INSTALLED" }, { "packageName": "dev.yusukeiwaki.mylauncher", "installType": "KIOSK", "managedConfiguration": { "mode": "home" } } ],
学校モードはmodeをschoolにしただけ。
"packageName": "dev.yusukeiwaki.mylauncher", "installType": "KIOSK", "managedConfiguration": { "mode": "school" } }
あとは、適当な時間帯で、Android Management APIを叩いてポリシーを当てるようにすればいいだけ。IoTスイッチ的なものでポチッとできるようにするとさらに素敵かもしれない。
まとめ
Android Management APIがあれば専用端末作り放題で楽しい。
Rakuten miniは安くてコスパ最高
Ruby 2.7, 3.0, 3.1でもデータクラス (Data.define) が使えるようなライブラリdata_class_factoryを作った
まえおき
Ruby 3.2のデータクラスっていうやつがとてもハイセンス。実装した人自身が、dev.toでとても自慢げに記事を書いている。
大昔に自分で初めて作ったライブラリが奇しくもデータクラスだったので、これは見逃せない。
Ruby 3.2のデータクラスは、Structから機能を落とした版で、おそらく多くのRubyistたちが望んだものだろう。しかしながら当然のこと、これはRuby 3.1以前のバージョンでは使えない。
いっぽうで、そこそこ大きめのRailsアプリケーションを開発してると、Rubyのバージョンはそんなにホイホイ上げられるものではないので、Ruby 3.0や3.1止まりの会社も今はまだ多かろう。そんなわけで、kt_data_classのエッセンスは取り入れつつ、バックポートを目的としたライブラリを作ることにした。
1日で作りたい
kt_data_classを初めて作ったときは、たぶん1週間くらいかけて作ってたのだけど、今回のデータクラスのバックポートは、なる早で使いたい。
目標として「1日で仕上げる」ことを気に留めながら開発を進めた。
テスト駆動だが、今回はMinitest
スピード開発なので、迷わずテスト駆動で開発をすすめることにした。
kt_data_classのときは bundle gemのデフォルト値に従うままでRSpecでテストを書いていたが、Minitestの並列実行の速さに慣れてしまうとRSpecはとっても遅い。今回は速いMinitestを採用。
あと、bundle gemのデフォルトの構成だとtest_helperとか色々作られるが、よりシンプルな構成に変更。
Rakefileはこんなかんじ。 rake
単体でテストが流れるのは若干キモイので、 rake test
でテストが流れるだけの設定にダイエット。
require 'bundler/gem_tasks' require 'rake/testtask' Rake::TestTask.new(:test) do |t| t.libs << 'test' t.libs << 'lib' t.test_files = FileList['test/**/*_test.rb'] end
個々のテストでは、test_helperを使わず、↓こんな感じのをテンプレートとしてこぴぺして使った。
require 'minitest/autorun' require 'data_class_factory' class XXXTest < Minitest::Test def test_zzzzzz
ここまでやることで、「テストファイル1つを見れば、単体テスト内容がすべてわかる」という状態にできる。あっちこっち見て回らなくて住むのでスピーディーにテストが書ける。
リファレンスとRuby本体の単体テストを大いに参考にした
Dataクラスについては、Rubyのリファレンスがとても充実している。
あとは、RubyにDataクラスが追加される際に書かれた単体試験(Add Data class implementation: Simple immutable value object by zverok · Pull Request #6353 · ruby/ruby · GitHub)も、ほぼそのまま流用することで、Dataクラスがどう振る舞うべきか、Ruby 3.2のDataクラスとの乖離が少ない形で実装が進められる。
dup, cloneと、Marshal.dump/loadの対応
Ruby本体の単体試験には以下のようなコードがある。
def test_dup klass = Data.define(:foo, :bar) test = klass.new(foo: 1, bar: 2) assert_equal(klass.new(foo: 1, bar: 2), test.dup) assert_predicate(test.dup, :frozen?) end Klass = Data.define(:foo, :bar) def test_marshal test = Klass.new(foo: 1, bar: 2) loaded = Marshal.load(Marshal.dump(test)) assert_equal(test, loaded) assert_not_same(test, loaded) assert_predicate(loaded, :frozen?) end
https://github.com/ruby/ruby/blob/v3_2_0/test/ruby/test_data.rb
シャローコピー(dupやclone)してもディープコピー(Marshal.dump/load)しても、freezeされた状態であれ、というもの。
class Data def initialize(**kwargs) ...いろいろ freeze end
こんな感じでinitializeの末尾でfreezeしておけばいいのだと思っていたが、これではdupやclone, Marshal.dump/loadいずれの操作時にも、freezeされていないインスタンスが生成されてしまった。
結論から言うと
- dupやcloneでは
#initialize_copy
のオーバーライドが必要- よく見たら Object#initialize_copy (Ruby 3.2 リファレンスマニュアル) に詳しく書いてある。
- .newの場合は allocateしたあと、initialize が呼ばれるのに対し、dupやcloneはallocateしたあとinitializeは呼ばずinitialize_copyを呼ぶらしい
- Marshal.loadでは
#marshal_dump
#marshal_load
のオーバーライドが必要- marshal_loadだけをオーバーライドしても動かない。両方オーバーライドが必要。
ということだった。kt_data_classのときはここまで考慮してなかったので、今回とても勉強になった。
.newの引数はキーワード引数だけにする
Structを踏襲してなのか、Dataクラスも「キーワード引数でもキーワード引数じゃなくてもいい」という仕様が健在だ。
Point = Data.define(:x, :y) Point.new(3, 4) # OK Point.new(x: 3, y: 4) # OK
ただ、これは完全に個人的意見だが、キーワード引数だけで十分だと思うし、そのほうが呼び出し側だけ読んで引数の意味がわかりやすくて良いと思う。
Ruby 3.2のData.defineの「キーワード引数もキーワードじゃない引数も受け付けるよ」って仕様、ほとんどのケースであまりうれしくないんじゃないかって思うんだけど、どうしてこうしたん??せめてキーワード引数だけを受け付けるオプションほしい...。
— Yusuke Iwaki (@yi01imagination) 2023年2月26日
Feature #16122: Data: simple immutable value object - Ruby master - Ruby Issue Tracking Systemのディスカッションを見るに、どうもパターンマッチングとの整合性や、単純に書く量多くね?みたいな流れでキーワードじゃない引数もサポートするようにしたようだ。 また、New in Ruby 3.2 - Data.define - DEV Communityにも書かれているように、キーワードじゃない引数をサポートするための実装として、initializeではなく .newのほうで少々工夫が凝らされている。
もしキーワード引数だけの対応であれば、そんな工夫は一切いらない。
別にそれでよくね?
というわけで、バックポートDataクラスでは、一旦キーワード引数だけをサポートする形としてみた。
やっぱりpositional argumentもサポートしたほうが便利だなってなったら、今後追加するかもしれない。
で、一日でできたの?
思い立ったのが2023/3/4の10:53で
これを改造してRuby 3.1以下でもData.defineを使えるようにしたら需要あるんでは?と思ってる。すでに誰かやってそうではあるけどhttps://t.co/rgcBY2yshS
— Yusuke Iwaki (@yi01imagination) 2023年3月4日
仕事終わりの夜に実装を進めて、データクラスが出来上がったのは 3/6の2:31 github.com
Gemを世に放ったのは3/8の10:28なので、まぁ全然1日では終わっていないw
そうはいっても、kt_data_classのときよりはだいぶスピーディーに開発したとは思う。(自画自賛w
まとめ
思い立って数日で、なんとか形にはした。
そして、こういう既存機能ポーティングのライブラリ開発は作るたびに勉強になる。
自分で開発しているRuby用のライブラリに型情報を足す
仕事で忙殺され気味のため久々の更新。
playwright-ruby-clientにいきなり「型書け」ってissueが飛んできた。
個人的にはGitHub Copilotにどっぷり浸かっていて、「Rubyだったらべつに型いらんくね?」って考えなんだけど、playwright-ruby-clientに関してはユーザが触るコードは全部自動生成しているので、コード生成スクリプトにちょろっと追記したら手で型定義書かなくてもRBSも自動生成できるんじゃないか?と試してみた。
どういう状態にしないといけないか
RBSを"使う"側つまり、ライブラリを使ってコードを書く側の話はぐぐるとたくさんでてくるが、Gemを開発している人は何をやればいいのか?というのはそんなに情報がない。
唯一、この記事↓がまさにそれを紹介していた。
結論から言うと、ライブラリの開発者がやらないといけないことは至極単純で、 sig/your_library.rbs
のファイルを同梱するだけ 。
. ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin │ ├── console │ └── setup ├── lib │ ├── playwright │ ├── playwright.rb │ └── playwright_api ├── pkg ├── playwright.gemspec ├── sig ←★これを追加 │ └── playwright.rbs └── spec
どんな型定義を書かないといけないのか
型定義はいきなり書けと言われて所見で書けるものではない。まずはTypeProfで雛形を作成する。
typeprof lib/playwright_api/*.rb -I lib/playwright -o sig/_playwright.rbs
これだけで割とそれっぽい型定義は作られる。
あとは、 sig/_playwright.rbsを参考に、 sig/playwright.rbs
を頑張って作る。
さらにそれをTypeProfに食わせてみて、書いた型定義にミスが無いかを確認する。
typeprof sig/playwright.rbs lib/playwright_api/*.rb -I lib/playwright -o sig/_playwright.rbs
rbs validateなどもあるが、typeprofだけでも結構いける。
「こんなときどう書けばいいの?」って疑問は、9割がた↓の記事をみて解決できた。pockeさん神記事です。
TypeProfの "unhandled exception" 問題
TypeProfに食わせるrbsファイルに不備があると、 "unhandled exception" っていうエラーが出て2回ハマった。
playwright-ruby-clientの型情報それっぽいRBSを自動生成できたけど、TypeProgに食わせるとunhandled exceptionになるな... pic.twitter.com/Xgafi3rqX7
— Yusuke Iwaki (@yi01imagination) 2023年1月15日
結論、以下の2つがハマりどころでした。
- パラメータ名が "type" だとダメ
- ArrayやHashは
Array[untyped]
Hash[untyped, untyped]
って書かないとダメ
型を書いてみた結果...
RBSをサポートしてるぜって熱弁しているRubyMineをインストールして、対応前後の動きをみてみた。
もともとがこんな感じ。ほとんど何も補完が効かない。これならVSCodeやSublime Textで十分、ってかんじ。
対応後はこんな感じ。「コード補完ほしい」「型書け」って言う人達にとってはまぁ嬉しいんだろうなという結果。
でもGitHub Copilotはもっとすごいよ...
型を書く書かないにかかわらず、Copilotはこのレベルで補完をガンガンしてくる↓
- どちらかというと自分のライブラリのサンプルコードを世の中にバンバン出していくほうが、Copilotによる補完精度が上がって、ユーザにとってはありがたいんでは?
- まだ人気のないライブラリに限ってはRBS書いたほうがよさそう
という所感に落ち着く。
まとめ
Gemを自分で作ってる人は sig/your_library.rbs
を足すだけで「型書け」には答えられる。
蛇足: 「型書け」と言ったからには責任を!
GitHub Copilotにどっぷりな身としては、「なんとなくコードの補完効いてほしいよね〜」くらいのレベルで「型を書け」というのはRubyにおいては正しくないと思う。
Add type signatures (RBS) · Issue #239 · YusukeIwaki/playwright-ruby-client · GitHub
この手のissueを放り投げるのは、考えるきっかけとしてはありがたかったものの、「型書け」って人に言うからにはしっかり議論やフィードバックしてくれ!ってのが本音。