Upgrade to Pro — share decks privately, control downloads, hide ads and more …

FlutterKaigi 2022: ホテルのルームキーをデジタルキー化して得られた気づき

Kouta Imanaka
November 16, 2022

FlutterKaigi 2022: ホテルのルームキーをデジタルキー化して得られた気づき

FlutterKaigi 2022: ホテルのルームキーをデジタルキー化して得られた気づき
https://fortee.jp/flutterkaigi-2022/proposal/da3e71d0-821b-455f-9470-24a6d852a548

Kouta Imanaka

November 16, 2022
Tweet

More Decks by Kouta Imanaka

Other Decks in Technology

Transcript

  1. デジタルルームキー機能について 5 • 一般的には、磁気やNFCのカードキーで客室のドアを解錠する • デジタルルームキー機能では、 Bluetooth Low Energy (以下

    BLE) 技術を使用して スマホで解錠できるようになる • デジタルルームキーの利点 • 鍵の閉じ込めリスクが減る • 鍵の紛失・持ち帰りがなくなる • 非接触チェックイン・チェックアウト
  2. 今回説明をしないこと 8 1. 鍵を解錠するためのBLE通信方法 • BLE通信ですぐ思いつくものと言えば `flutter_blue` ライブラリがあると思いますが・・・ • 本件ではスマートロックのベンダーが提供するSDKを使用しました

    • ただしこのSDKがiOS/Android Native版のみの提供だったので、 これをFlutterで組み込む方法については解説いたします 2. 鍵解錠に用いるデータの取得・生成など • これもSDKに関連することなので説明しません(できません)
  3. 11 Android: ハードウェアがBLEを提供しているか? 11 <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> ただし、たいていの場合BLEが使えなくても他の機 能は使えるべきなので、`required:false`

    設定にし てフィルタされないようにします。 PackageManager#hasSystemFeature( FEATURE_BLUETOOTH_LE ): boolean この結果がfalseならBLEはシステムに搭載されて いない BLEが使用できない端末では ストア経由でインストール出来ないよう にする 実行時にBLEが使用できるかどうかを 確認する
  4. 12 Android: ソフトウェアレベルでBLEが有効になっているか? • BluetoothAdapter#isEnabled(): boolean で確認する • 結果がtrueならBluetoothが使用できる=BLEも使用できる •

    falseだった場合はBluetoothがOFFになっている • Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) を発行すればBluetoothのON/OFF画面を表示できます • しかし最近のOSだとアクセスしやすいところに トグルスイッチがあるので作り込まなくてもいいと 個人的に思っています
  5. iOS: ハードウェアがBLEを提供しているか? 13 • iOS9以上が動作するiPhone/iPadならBLEをサポートしている • ハードウェア的に使えないことを考慮しなくて良い • UIRequiredDeviceCapabilities で

    `bluetooth-le` をセットすると 非対応デバイスでのインストールを制限できる https://developer.apple.com/jp/support/required-device-capabilities/
  6. iOS: ソフトウェアレベルでBLEが有効になっているか? 14 • `.powerOn`: ON • `.powerOff`: OFF •

    `.unsupported`: BLEをサポートしてない • `.unauthorized`: ランタイムパーミッションでBLE権限が付与 されてない • `.unknown`: 状態が分からない • `.resetting`: システムとの一時的な切断 https://developer.apple.com/documentation/corebluetooth/cbcentralmanager , https://developer.apple.com/documentation/corebluetooth/cbmanagerstate CBCentralManager#state を確認する
  7. iOS: ソフトウェアレベルでBLEが有効になっているか? 16 • CBCentralManagerの生成を遅延させる • インスタンス生成直後はstateが `.unknown` として申告される •

    CBCentralManager.authorization を確認 すると許可ステータスだけ確認できる • これはクラス変数でインスタンス生成しない からダイアログが出ない • ただしiOS13.0-13.1までインスタンス変数 だった時代がある(問題が解決されていな い可能性がある) https://developer.apple.com/documentation/corebluetooth/cbmanagerauthorization enum CBManagerAuthorization • allowedAlways • 明示的にBluetooth権限を許可している • denied • 明示的にBluetooth権限を拒否している • notDetermined • まだ承認していない • restricted • Bluetoothの使用を許可されていない • ペアレンタルコントロールなどの理由で Bluetoothステータスを変更できない 注意点の回避策
  8. Flutter: BLEが使用できるか? • これらを完全に満たす既存ライブラリを見つけられませんでした • 特にiOSのCBCentralManagerの件が厄介 • 本件では実行時エラーとして対処しています • どうしても事前チェックが必要なところでは

    flutter_blue_plus を使用しています • isOn() , isAvailable() などが提供されています • 今後ライブラリのアップデートなどで状況が変わったとき、 Native実装での観点を見て実際に対応されたかどうかを確認すると良 いと思います 17
  9. Flutter: permission_handler を用いる 19 • たぶんド定番な permission_handler を使うのが良い • 基本的にREADME.mdを読めばやるべきことが全て書いてある

    • ちょっとPodfileあたりがややこしいか • BLEを使用するうえで確認すべき権限がplatformとOSバージョンで少し 異なる • というか主にAndroidがややこしいことになっていて、 どうしていいか分かりづらいので、解説します https://pub.dev/packages/permission_handler
  10. Android 12 (API level 31) 以上での権限宣言 20 • 以下の権限のうち必要なものを宣言します •

    `android.permission.BLUETOOTH_ADVERTISE` • 他の機器に自分のデバイスが検出されるようにするのに必要な権限 • 今回の要件では不使用のため定義しませんでした • `android.permission.BLUETOOTH_CONNECT` • ペアリングされている機器とやりとりするために必要な権限 • `android.permission.BLUETOOTH_SCAN` • BLEペリフェラルを見つけるために必要な権限 • 位置情報を補完する目的で使うのでないなら android:usesPermissionFlags="neverForLocation" とすると位置情報権限が必要でなくなる https://developer.android.com/guide/topics/connectivity/bluetooth/permissions
  11. • Android11以下では(旧来の)Bluetoothの権限が必要です • `android.permission.BLUETOOTH` • `android.permission.BLUETOOTH_ADMIN` • 厳密にはこの権限はAndroid12以降でも使うことがありますが、 本件では関係がなかったので指定しません •

    Android11以下ではBLEを使うのに位置情報の権限が必要です • `android.permission.ACCESS_COARSE_LOCATION` • `android.permission.ACCESS_FINE_LOCATION` Android 11 (API level 30) 以下での権限宣言 21
  12. AndroidManifest.xml のまとめ 22 <manifest ...> ... <!-- API (28) -

    30 --> <uses-permission android:maxSdkVersion="30" android:name="android.permission.BLUETOOTH" /> <uses-permission android:maxSdkVersion="30" android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:maxSdkVersion="30" android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:maxSdkVersion="30" android:name="android.permission.ACCESS_FINE_LOCATION"/> <!-- API 31 - * --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <!-- API (28) - * --> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> </manifest> https://gist.github.com/keima/b14ef729ba172a170600cfdd6743b7c6#file-androidmanifest-xml
  13. Android: 位置情報の権限を許可してもらう際の画面について 23 • BLEを使うのに位置情報の権限を要求さ れるのは直感的でない • 位置情報の権限を要求する場合は丁寧 に理由を説明するようにする •

    この画面の遷移条件を管理しないと 繰り返し遷移してしまうケースが起こりや すいので、preferenceなどで画面遷移を 管理するようにする
  14. permission_handler 使用時の permanentlyDenied 判定 24 • いわゆる shouldShowRequestPermissionRationale() が false

    を返す とき • permission_handler のコードを追う限り、以下の PermissionStatus を 返す操作の挙動は微妙に異なる • await Permission.locationWhenInUse.status • denied か granted のみ返す • await Permission.locationWhenInUse.request() • denied, granted に加えて permanentlyDenied を返す • permanentlyDenied を確認するには request() を使う必要がある点に注意する
  15. Flutterでの権限確認方法 26 enum OperatingSystem { ... factory OperatingSystem.generate(int? androidSdkVersion) {

    if (Platform.isIOS) { return ios; } else if (Platform.isAndroid) { if (androidSdkVersion != null && androidSdkVersion <= 30) { return androidUpToApiLevel30; } else { return android; } } throw UnsupportedError("Unknown platform"); } } https://gist.github.com/keima/b14ef729ba172a170600cfdd6743b7c6#file-operating_system-dart operatingSystem.permissions.request() とすれば一気に確認できる
  16. その他の留意事項 27 • 今回はAndroid11とAndroid12で明らかに挙動が変わるため、 この2バージョンについてはしっかり確認しておく • Androidでは権限認可をadbコマンドで剥がしたり付けたりできるので活 用する • adb

    shell pm grant {packageId} {permissionName} • adb shell pm revoke {packageId} {permissionName} • permissionName: android.permission.ACCESS_FINE_LOCATION • iOSではアプリをインストールした状態で権限を リセットする方法がない(っぽい)のがめっちゃつらいので 気合いで頑張る
  17. FlutterプラグインがないSDKをFlutter対応させる 29 • Flutterも活気がある中で、使いたいライブラリがpub.devにないことはま だまだある • mavenやCocoaPodsにはあるケース • Android Archive

    (.aar) や Binary Framework (.xcframework) • いづれにせよFlutterで使いたい場合は、 自分たちでFlutterブリッジを記述する必要がある • プロジェクトの `./ios` および `./android` に直接実装する • プラグインとして実装する • Flutterのバージョンアップ耐性が高そうなこちらの方法を選んだ • 今回はアプリ内にセルフホストする形式をとりました https://docs.flutter.dev/development/packages-and-plugins/developing-packages
  18. `./plugins` フォルダ内に plugin テンプレートを構築する 31 $ cd ./plugins $ flutter

    create \ --template=plugin \ --platforms=android,ios \ -i swift -a kotlin \ --org com.example.flutter.opensesame \ open_sesame_plugin • プラグイン名は `open_sesame_plugin` とする • snake_case で設定する • 生成されるフォルダも `./plugins/open_sesame_plugin` となる
  19. Native SDKをセットアップする 32 • mavenやCocoaPodsにあるSDKの導入は簡単 • `build.gradle` の dependencies に記述する

    • `plugin_name.podspec` の s.dependency に記述する • 今回は aar や xcframework を参照する • これが一筋縄ではいかないので、やや長くなるが解説します (以下特に断りがない限りはフォルダrootは  ./plugins/open_sesame_plugin/ とします)
  20. Android: AARを組み込む 33 • ./android/sdk-bridge/ フォルダを作成する • sdk.aar を設置し、 build.gradle

    を以下内容で作成する configurations.maybeCreate("default") artifacts.add("default", file('sdk.aar')) • ./android/build.gradle に以下追記する android { … } dependencies { // ← implementation project(":sdk-bridge") // ← } // ← • 更に ./android/settings.gradle にも以下追記する rootProject.name = 'open_sesame_plugin' include ":sdk-bridge" // ←
  21. Android: アプリプロジェクトからAARを参照させる 34 • シンボリックリンクでアプリプロジェクトからAARを参照させる # アプリプロジェクトの例 cd /path/to/hostapp/android/ ln

    -s ../plugins/open_sesame_plugin/android/sdk-bridge # プラグインのサンプルプロジェクトの例 cd /path/to/hostapp/plugins/open_sesame_plugin/example/android ln -s ../../android/sdk-bridge • 最後に、以下の2ファイルについてこのように追記する • /path/to/hostapp/android/settings.gradle • ./example/android/settings.gradle include ':app' include ':sdk-bridge' // ←
  22. iOS: XCFramework を組み込む 35 • ./ios/Frameworks フォルダを作成する • そこに sdk.xcframework

    を配置する • ./ios/open_sesame_plugin.podspec に以下定義を追記する Pod::Spec.new do |s| ... s.vendored_frameworks = 'Frameworks/sdk.xcframework' s.preserve_paths = 'Frameworks/*' end • 状況によっては他の定義も必要です • 例えばVALID_ARCHSとか s.framework とか • 今回はうまくパッケージングしてくれていたのでこれだけでOK https://guides.cocoapods.org/syntax/podspec.html
  23. 余談: XCではないFramework形式の場合 36 • framework形式のバイナリはfat fileという、複数アーキテクチャをひとつにパッ ケージングしたファイルになっている % lipo -info

    ***** Architectures in the fat file: ***** are: x86_64 i386 armv7 arm64 • このファイル形式の問題点 • シミュレーター向けバイナリを含む状態でAppStoreに公開できない • これを解決する為に `lipo -extract` および `lipo -o` で適切なアーキテクチャ群に再パッケージ する必要がある • バイナリがシミュレーター向けなのか実機向けなのか区別が付かない • M1 Mac上のシミュレーターとiPhone実機のアーキテクチャは `arm64` • SDKベンダーにXCFramework形式への移行を 【強く】お願いする必要がある https://qiita.com/mgre_hiromi/items/14472bc4983d05c8e910
  24. exampleフォルダを軸にプラグイン実装する 37 • Dartだけを編集する場合 • VSCodeやAndroidStudio+Flutter pluginで ./ を開く •

    Android Nativeレイヤーの実装 • Android Studioで ./example/android/ を開く • ./android を開いてもうまく編集できないので注意する • iOS Nativeレイヤーの実装 • さきに ./example/ios で `pod install` しておく • Xcodeで ./example/ios/Runner.xcworkspace を開く • ./ios 側のファイルを開いてもうまく編集できないので注意する https://docs.flutter.dev/development/packages-and-plugins/developing-packages rootフォルダは ./plugins/open_sesame_plugin/ です
  25. FlutterとNativeレイヤーで通信する 38 • 基本的には MethodChannel を用いる • 定期的に発行されるイベントをlistenしたいなら EventChannel •

    実装するうえで参考になる情報 • https://docs.flutter.dev/development/platform-integration/platform-cha nnels • ここにだいたい全てが書かれている • 似たような動作をするプラグインの実装を読む • 極力有名どころのプラグイン実装を読むのをオススメしたい
  26. 43 • たまに出張があるとウキウキする • しっかり事前検証したとはいえど、 実際に現地で解錠できると、とても嬉しい • 多少のUI修正が必要となった • ステークホルダー間での認識齟齬

    • キーデータが取得できるようになるタイミング • 解錠できる時間の認識違い • 電波環境が理想的な状態ではなかったり • 「これはこうだろう」と思い込まず、 しっかり当事者から確認をとることも大事だと思った
  27. OS差異や機種依存はどうだったか? 44 • 現地検証において致命的な問題はほとんど起きなかった • 基本的にplatform一律でバグってることが殆どだった • Androidの機種依存は予想に反してほとんど観測されなかった • 持参した検証機で解錠失敗は起こらなかった

    • iOS: NFC兼用のルームキー端末でApple Payが起動する問題 • これは事前に聞いていて対策済みで現地検証に臨んだ • PKPassLibraryの `requestAutomaticPassPresentationSuppression` APIを使うと、当該アプリ起動中はApplePayが反応しなくなります • ただしこのAPIは特別なentitlementが必要です https://developer.apple.com/documentation/passkit/pkpasslibrary/1617078-requestautomaticpasspresentation