📖

[Android]「Chromebookでも使えるアプリ」にする

5 min read

はじめに

この記事は、 フラー株式会社 Advent Calendar 2021 の23日目の記事です。

22日目の記事は @gaku07jp さんの CloudFormationのパラメータ管理をjsonにした話 でした。

とある業務で「既存のアプリを Chromebook でも使えるようにする」というアップデートを行ったので、その時得た知見を紹介します。

Chromebook?

Chromebook は Chrome OS という独自OSで動くノートPCであり、Google Play ストアから Android アプリをダウンロードして使えるのが大きな特徴です。

「Chromebookでも使える」とはどういう状態か?

Chromebook対応をするにあたって、以下のような状態を「Chromebookでも使える」と定義しました。

  • 主要機能がChromebookでもスマホと遜色なく動く
  • 操作不能になるなどの大きな不具合がない

今回は「Chromebookでもアプリを動かすことができる」という状態を目指し、Chromebookへ動作やUIを最適化する、などの対応については一旦考えないものとしました。

調査

まずは既存のアプリがChromebookでどういう状態に見えるのかを調査しました。

Chromebookには大きく分けて以下の3種類があります。

  • タブレット型
  • ラップトップ型
  • 一体型(コンパーチブル型)

このうち、タブレット型は通常のAndroidタブレットとほぼ変わらないため、調査はラップトップ型とコンパーチブル型に絞ることにしました。

調査した結果、コンパーチブル型の端末ではAndroidタブレットのように問題なくアプリのインストール・操作が可能だったのですが、ラップトップ型ではPlayストアに表示すらされていない状態でした。

このため、本記事で「Chromebookでも使えるようにする」という場合の「Chromebook」とは、ラップトップ型のChromebookを指すこととします。

ストアに表示されない原因

原因を調査してまず分かったことは、大前提として、「スマホ特有のネイティブ機能を利用していなければ、ほとんどのAndroidアプリはそのままChromebookでも使える」 ということです。

「スマホ特有のネイティブ機能」とは、カメラやGPSといった、端末の中に組み込まれているなんらかのデバイスを使った機能のことです。

今回の場合、アプリ内にカメラを利用した機能が存在し、カメラのパーミッションを要求していた事が原因でストアにアプリが表示されない状態になっていました。

ちなみにカメラのパーミッションを普通に要求した場合、「フロントカメラと背面カメラ両方が揃った端末でないとインストールできない」という罠があり、Chromebookのラップトップ型は「(ZoomなどのWebカメラ利用を想定した)フロントカメラは搭載しているが背面カメラは搭載していない端末」というものがそこそこあるため、何も考えずにパーミッションを要求すると、今回のようにChromebookではDLすら出来なくなります。

どうすればよいか

これらネイティブ機能を使っているアプリにも2種類あると思います。

  • ネイティブ機能の利用が必須であるアプリ
  • ネイティブ機能の利用が必須ではないアプリ

例えば写真や動画を撮影するアプリのように、カメラ機能を主要なコンテンツとして利用しているアプリは Chromebook に対応することは難しいかもしれません。

しかし、SNSアプリで「プロフィールアイコンの撮影にカメラも利用できるけど、デバイス内の写真も利用できる場合」など、 機能の一部としてカメラを利用するが、利用は必須ではない アプリの場合は、その機能さえ無効にしてしまえば「Chromebookでも使えるアプリ」にすることは可能ということになります。

今回のアプリはカメラ機能の利用が必須ではなかったため、Chromebookに対応することができました。

Chromebook対応するためにやったこと

Chromebook対応のためにやったことは主に以下の3つです。

  • アプリのパーミッション見直し
  • カメラ機能を利用している箇所に機能の有効・無効チェックの追加
  • ハードウェアキーボード対応

アプリのパーミッション見直し

前述したように、ただ単純にカメラ機能のパーミッション要求を追加すると、フロントカメラと背面カメラの両方がちゃんと揃っていない端末の場合、「カメラ機能が使えない」と判定されPlayストアからアプリがDLできません。

通常のパーミッション要求

<uses-permission android:name="android.permission.CAMERA" />

これを回避するためには、 <uses-feature>を AndroidManifest.xml に追加し、カメラ機能の利用が必須ではない事を明示的に宣言する必要があります。

カメラ機能を必須ではないと設定する場合は以下のような記述になります。

カメラ利用を任意にしたパーミッション要求

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature 
    android:name="android.hardware.camera"
    android:required="false" />

上記のように required="false" 属性を追加することで、カメラ機能の搭載されていない端末でもアプリのインストールが可能になります。

カメラ機能を利用している箇所に機能の有効・無効チェックの追加

カメラのない端末でアプリを起動した時のために、カメラを利用する箇所に機能チェックを入れます。

フロントカメラの場合は以下のようなメソッドを定義することで判定が可能です。

fun hasFrontCameraHardware(context: Context): Boolean {
    val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
    // デバイスに搭載されている全てのカメラIDを取得
    val cameraIds = cameraManager.cameraIdList
    // カメラIDからフロントのカメラIDを抜き出して、
    // 1つでもIDがあれば搭載ありと判断
    return cameraIds.any { cameraId ->
        val characteristics = cameraManager.getCameraCharacteristics(cameraId)
        characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
    }
}

CameraManager で端末に搭載されているすべてのカメラからIDを取得して、その中にフロントカメラが1台でも存在するかどうかを判定しています。

CameraCharacteristics.LENS_FACING_FRONT でフロントカメラかどうかの判定ができ、 CameraCharacteristics.LENS_FACING_BACK で背面カメラかどうかの判定ができます。

このメソッドを用いてカメラの有無を判定し、機能の有効・無効をチェックしました。

if(hasFrontCameraHardware(requireContext())) {
    // 機能を有効にする
}
else {
    // 機能を無効にする    
}

本当は packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT) で判定できないかなと思ったのですが、フロントカメラ非搭載端末であっても、どうあがいても true が返ってきてしまい、うまく判定できませんでした😢

ハードウェアキーボード対応

もうひとつ盲点だったのが、Chromebookだとソフトウェアキーボードではなくハードウェアキーボードで操作することにより、状況によっては操作不能に陥る ということでした。

どういうことかというと、物理キーボードのエンターキーを押した判定と、ソフトウェアキーボードのエンターにあたる部分を押した判定(いわゆる EditorInfo.IME_ACTION_NEXTEditorInfo.IME_ACTION_DONE)は異なるらしく、 EditTextSearchView などのテキスト入力用のViewで物理エンターキーを押しても確定扱いにならないということです。

よくあるのが、入力を確定すると同時に次画面が表示される(例:検索画面→検索結果画面など)というUIだと思うのですが、

これがChromebookでは実行できない状態になっており、原因は単純なわりに「画面遷移できない」という重大な障害となっていました。

やったこと

setOnKeyListener をセットし、エンターキーイベントを拾って、ソフトウェアキーボードの時と同じメソッドを発火するようにしました。

view.setOnKeyListener { _, keyCode, _ ->
    if (keyCode == KeyEvent.KEYCODE_ENTER) {
        // エンターキーが押された動作
        ...
        true
    } else {
        false
    }
}

おわりに

今回はアプリをChromebookへ対応させた時に得た知見をまとめました。

GIGAスクール構想やテレワークの浸透などで、Chromebookの普及率は以前よりも上がってきていると言われています。AndroidアプリをChromebookに対応させたいという需要も今後高まるかも?しれません。
そんな時に本記事が役に立てば幸いです。

明日の記事は @su8 さんの「コンテナ遠洋航海 〜プロセス分離の旅路〜」です。お楽しみに!

Discussion

ログインするとコメントできます