Chromebookで始めるラップトップ向けAndroidアプリ開発

## はじめに

Droidkaigi2019で話してきた内容はこちらです。（[スライド](https://docs.google.com/presentation/d/19YA_kW45zlpzq9PcAMZN3k7_M37IpnZulodeb0jwQg4/edit?usp=sharing)）
GitHubの[wiki](https://github.com/Reyurnible/chromebook-android-sample)にコードなどの内容を載せています。

Chromebook向けのAndroidアプリの開発と、ChromebookでのAndroidアプリの開発についてまとています。
それぞれ、主にCodelabsや参考サイトをまとめた内容となります。

# ChromeOSとは

Linuxをベースに、Googleが開発しているOSで、ラップトップ（Chromebook）やデスクトップ（Chromebox）向けに提供されている。

## 簡単な歴史：

- 2009年：Google Chrome OSのオープンソース版である「Chromium OS」（クロミウム・オーエス）のソースコードを公開
- 2016年 Google I/O：Androidアプリのサポートを発表
- 2018年 Google I/O：Linuxのサポートを発表（現在も、beta状態）

## AndroidからみたChromebookの特徴

![AndroidからみたChromebookの特徴](https://github.com/Reyurnible/chromebook-android-sample/raw/master/images/android_specific_chrome.png)

## ChromeOSとAndroidの差分について

ChromeOS -> Androidアプリに合わせている部分

![ChromeOS->Android](https://github.com/Reyurnible/chromebook-android-sample/raw/master/images/chromeos_to_android.png)

Androidアプリ -> ChromeOSに合わせている部分

![Android->ChromeOS](https://github.com/Reyurnible/chromebook-android-sample/raw/master/images/android_to_chromeos.png)

以下のページで、ChromeOS独自の対応について説明

* 04_ウィンドウ・レイアウトサポート
* 05_入力サポート

## ChromeOSでのAndroidアプリのデバッグについて

![デバッグ全体像](https://github.com/Reyurnible/chromebook-android-sample/raw/master/images/debug_all.png)

以下で説明

- 01_ChromeOSのアプリデバッグ方法
- 02_ChromeOS上でのAndroidアプリの開発

Chromebook向けのアプリの対応として、マルチウィンドウ、フリーフォームレイアウトの対応について、複数サイズの画面の対応について記載。

# ウィンドウ・レイアウトサポート

## Chromebookの画面に関する特徴

ChromebookではPCと同様に、複数ウィンドウの立ち上げと、ウィンドウサイズの変更できることが特徴となります。

![chromeos_window_support](https://github.com/Reyurnible/chromebook-android-sample/raw/master/images/chromeos_window_support.png)

## APIレベル別のウィンドウ対応

アプリビルド時の`targetSdkVersion`によって挙動が変更される。

| API level |	Window manager behavior |
|:----------|:------------------------|
| <= Android 1.5 (API level 3) | 全画面表示固定 |
| Android 1.6 (API level 6) ~ Android 6.0 (API level 23) | リサイズが保証されていないアプリとして扱われる。起動時のデフォルトは、縦画面のphoneサイズとして起動する。端末の画面表示切り替えボタンによって、画面サイズが切り替えられた場合はアプリを再起動させる。 |
| Android 7.0 (API level 24) resizable | マニフェストに何も指定をしていない場合はデフォルトでリサイズをサポートしているものとして扱い、ウィンドウをアプリの再起動なしでリサイズさせます。アプリの起動時は、デフォルト全画面で、M60以下のChromeOSの場合だけ、Nexus 5Xの縦画面のサイズで表示されます。 |
| Android 7.0 (API level 24) un-resizable |	マニフェストに、android:resizeableActivity="false"を指定した場合、リサイズが行われないようになります。ただし、特定のユーザー処理が行われるとウィンドウのリサイズ処理が行われます。 |
| Android 7.0 (API level 24) app-controlled |	マニフェストで指定したとおりの処理が実行されます。 |

アプリの、最大化状態や画面サイズは記録されます。

## Root Activityルール

ChromebookのウィンドウはActivityのスタックごとに別れ、それぞれのスタックごとに、ウィンドウの **向き** と **サイズ** を管理しています。

また、各スタックの一番下のActivity設定が、そのスタックに上のすべてのActivityの属性を決定するため注意必要となります。
例えば、一番下のスタックのRootActivityが画面の向き変更自由で、後からスタックに積まれたSubActivityが縦画面固定の設定をしていても、この設定は無視されます。また、ウィンドウサイズもすでにあるスタックの属性をそのまま受け継ぎます。

![root_activity_rule](https://github.com/Reyurnible/chromebook-android-sample/raw/master/images/root_activity_rule.png)

デバイスモードの場合の注意：
タブレットモードの向きはロックされず、Android通常の画面回転と同様に処理されます。
ただし、`targetSdkVersion`がAndroid 6.0(API level 23)以下の場合はロックされます。

### 別ウィンドウで立ち上げたい場合

Activityの立ち上げを別スタックにすればよいため、Intentの`Intent.FLAG_ACTIVITY_NEW_TASK`Flagを指定することで、別ウィンドウ立ち上げが行える。

## Androidのマルチウィンドウの種類について

マルチウィンドウでは、Activityを別ウィンドウで立ち上げることによる別画面で情報を同時に見せたり、アプリ内の別画面や、複数のアプリの間でドラッグ＆ドロップによるデータの共有を行うことができます。

マルチウィンドウは、Android 7(Nouger)で大きく変更が入り、特にPCやTV向けのサポートが多くありました。
複数のウィンドウを表示するには、主に以下のタイプがあります。

|                | スマホ | Chromebook |
|:--------------:|:------:|:--:|
| フリーフォーム | x | ○ |
| 分割画面 | ○ | x |
| ピクチャーインピクチャー | ○ | x |

Chromebookでは、フリーフォームのウィンドウのみ対応となる。

## Chromebookのウィンドウサイズの種類

![screen_size](https://github.com/Reyurnible/chromebook-android-sample/raw/master/images/screen_size.png)

Chromebook上でのウィンドウサイズ。

- フリーフォーム
  - スマホサイズ
  - タブレットサイズ
- ハーフレイアウト -> タブレットサイズ
- 全画面レイアウト -> PCサイズ

ユーザー操作：

- F3キー：全画面<->フリーフォームレイアウトの切替が可能
- 画面の両端へのドラッグ：ハーフレイアウトにすることが可能
- デバイスモードへの変更：タブレットモード<->PCモードの切り替えが可能
  - 画面回転 + 画面サイズ変更が発生
- （ウィンドウの端をドラッグ）：画面サイズを自由に変更可能

## ウィンドウサイズの指定

### Android 7.0のウィンドウ設定

Android 7.0 (API level 24) 以降の、すべて対応を行う場合の各種設定。

```xml
<manifest>
    <!-- マルチウィンドウに対応しているかの指定。
         Applicationレベルか、Activityレベルで設定 -->
    <application
            android:resizeableActivity="[true|false]">
        <!--  -->
        <activity
            android:name".RootActivity"
            android:resizeableActivity="[true|false]">
            <!-- 画面の左端から、バックボタンを消す変更 -->
            <meta-data 
                android:name="WindowManagerPreference:SuppressWindowControlNavigationButton"
                android:value="[true|false]" />
             <!-- 起動時のウィンドウサイズ指定 -->
            <meta-data 
                android:name="WindowManagerPreference:FreeformWindowSize"
                android:value="[phone|tablet|maximize]" />
            <!-- 起動時のウィンドウの向き指定 -->
            <meta-data
                android:name="WindowManagerPreference:FreeformWindowOrientation"
                android:value="[portrait|landscape]" />
        </activity>
        <activity
            android:name".FreeActivity">
            <!-- DP値でウィンドウサイズや位置を指定する場合 -->
            <layout 
                android:defaultHeight="500dp"
                android:defaultWidth="600dp"
                android:gravity="[top|bottom|start|end]"
                android:minHeight="450dp"
                android:minWidth="300dp"/>
        </activity>
    </application>
</manifest>
```

### 動的に起動するウィンドウサイズを変更する

新たなアクティビティを起動する際に、 `ActivityOptions.setLaunchBounds()` を指定したActivityOptionsを使用することによって新しいアクティビティの画面上での位置を指定することができます。
ただし、マルチウィンドウモードではない場合などは無視されます。

```kotlin
val options = ActivityOptions.makeBasic().apply {
    launchBounds = Rect(0, 0, 500, 500)
}
startActivity(it, options.toBundle())
```

※新規スタックでなければウィンドウサイズ等の変更はできず、既存のスタックの属性が継承されることに注意


分割画面で、ウィンドウを隣に表示する `Intent.FLAG_ACTIVITY_LAUNCH_TO_ADJACENT` 

## ドラッグ&ドロップサポート

複数のウィンドウ表示ができるため、画面内だけではなく、画面感のドラッグ&ドロップも簡単にできるようになるため、Chromebookでは重要なユーザー操作となる。
別アプリとのドラッグ&ドロップもサポートするために、ドラッグするデータの形式などを指定する必要があります。

共有できるデータは、主に以下の2種類になります。

- テキスト
- URL

※別アプリへ共有する場合は、相手のアプリがそのMIMETYPEをサポートしている必要があります。

![drag_and_drop](https://github.com/Reyurnible/chromebook-android-sample/raw/master/images/drag_and_drop.png)

### ドラッグする側の処理

1. ドラッグするデータの作成
    - [ClipDataクラス](https://developer.android.com/reference/android/content/ClipData)の作成
    - 指定できるデータと、それぞれのMIMETYPE
        - テキスト：ClipDescription.MIMETYPE_TEXT_PLAIN
        - HTMLテキスト：ClipDescription.MIMETYPE_TEXT_HTML
        - URL：ClipDescription.MIMETYPE_TEXT_URILIST
2. Drag中のViewの指定
    - `View.DragShadowBuilder`クラスを使用する
3. `View.startDragAndDrop()` の呼び出し

```kotlin
val targetView = it as TextView
// 1. ドラッグするデータの作成
val dragContent = "Dragged Text: ${targetView.text}"
val item: ClipData.Item = ClipData.Item(dragContent)
val dragData: ClipData = ClipData(dragContent, arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN), item)
// 2. Drag中のViewの指定
val dragShadow = View.DragShadowBuilder(targetView)
// 3. View.startDragAndDrop()の呼び出し
targetView.startDragAndDrop(dragData, dragShadow, null, View.DRAG_FLAG_GLOBAL)
```

`View.startDragAndDrop()`は、Android7.0より前に存在した`View.startDrag()`のエイリアスで、**DropPermission**などのマルチウィンドウ操作向けのフラグをつけるために拡張されたメソッドです。
**DropPermissions**については、後ほど説明します。

#### その他の操作：

それぞれ、DragAndDrop中のイベントに対してキャンセルとシャドーの変更ができます。

- `View.cancelDragAndDrop()`：実行中のドラッグ操作をキャンセルします。
- `View.updateDragShadow()`：実行中のドラッグ操作のドラッグシャドウを置き換えます。

※ドラッグ操作を開始したアプリだけが呼び出せます。

### ドロップされる側の処理

別のウィンドウからイベントを受け付ける可能性があるため、イベント内のデータのMimeTypeなどを確認する必要があります。
`DragEvent.ACTION_DRAG_STARTED`と`DragEvent.ACTION_DRAG_ENDED` イベントは、他のウィンドウ（別アプリ含む）でドラッグがスタートしたり、終了したタイミングで呼び出されます。

```kotlin
view.setOnDragListener { view, event ->
    when (event.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Limit the types of items that can be received
            if (event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                // Greenify background colour so user knows this is a target
                view.setBackgroundColor(Color.argb(55, 0, 255, 0));
                return true
            }
            false
        }
        DragEvent.ACTION_DRAG_ENTERED -> {
            // Increase green background colour when item is over top of target
            view.setBackgroundColor(Color.argb(150, 0, 255, 0))
            true
        }
        DragEvent.ACTION_DRAG_LOCATION -> {
            true
        }
        DragEvent.ACTION_DRAG_EXITED -> {
            // Increase green background colour when item is over top of target
            view.setBackgroundColor(Color.argb(55, 0, 255, 0))
            true
        }
        DragEvent.ACTION_DROP -> {
            requestDragAndDropPermissions(event)
            val item: ClipData.Item = event.clipData.getItemAt(0)
            if (event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                (view as TextView).also { target ->
                    target.setTextSize(TypedValue.COMPLEX_UNIT_SP, 30f)
                    target.text = item.text
                }
                return true
            }
            false
        }
        DragEvent.ACTION_DRAG_ENDED -> {
            // Increase green background colour when item is over top of target
            view.setBackgroundColor(Color.argb(0, 255, 255, 255))
            true
        }
        else -> false
    }
}
```

### DropPermissions

ドラッグ&ドロップでURIを共有する場合、受け取り側でURIがどのようなものかを判断してパーミッションをリクエストすることはできません。そのため、パーミッションをドラッグする側が取得し、それをドロップされる側に渡す仕組みが**DropPermissions**です。
ドロップされる側は、渡されたURIに対するパーミションの実装をすることなくURIのデータを扱うことができます。

また、パーミッションが付与されるのはDragEventで渡されたデータのURIだけで、他のURIに対しては権限が付与されないためユーザーにとっても安心して利用できる機能になっています。

![drag_and_drop_permission](https://github.com/Reyurnible/chromebook-android-sample/raw/master/images/drag_and_drop_permission.png)

#### ドラッグする側の処理

`View.startDragAndDrop()` の第4引数のフラグをOR(|)で連結して指定します。
それぞれ、権限周りのフラグは以下の通りです。

1. 他アプリとのDrag許可
    - View.DRAG_FLAG_GLOBAL
2. URIへの権限許可 ※1の指定必須
    - View.DRAG_FLAG_GLOBAL_URI_READ：URIに対する読み込み権限を付与
    - View.DRAG_FLAG_GLOBAL_URI_WRITE：URIに対する書き込み権限を付与
3. URIへの権限許可オプション系 ※2の指定必須
    - View.DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION：`revokeUriPermission`するまで権限を付与する
    - View.DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION：フォルダ単位で権限を付与する

例：

```kotlin
targetView.startDragAndDrop(dragData, dragShadow, null, View.DRAG_FLAG_GLOBAL|DRAG_FLAG_GLOBAL_URI_READ)
```

※前提として、`grantUriPermission` などでURIへのパーミッションを取得しているものとします。

#### ドロップされる側の処理 

ドロップされる側は、`Activity.requestDragAndDropPermissions(DragEvent)`を呼び出すだけで必要なパーミッションを取得できます。

```kotlin
view.setOnDragListener { v, event ->
  when (event.action) {
    DragEvent.ACTION_DROP -> {
        // 権限が取得できない場合や不正な場合は、nullが変える
        val permissions: DragAndDropPermissions? = requestDragAndDropPermissions(event)
        // 使用が終わったり、不要になったらreleaseする
        permissions?.release()
        true
    }
}
```

## デザインについて

GoogleのMaterial Designのの公式サンプルだとレスポンシブにGridを使用したデザインが多い。
RecyclerViewだと表示するアイテムのリストの列数を動的に変更するといった対応が多い。
「レスポンシブ」 + 「コンテンツを固定」 => 「コンテンツ数」 & 「余白」で調整。
詳しくは、[Responsive layout grid](https://material.io/design/layout/responsive-layout-grid.html#grid-behavior)を見る。

Google Material DesignのShrineの例：

![materialdesign_shrine.gif](https://qiita-image-store.s3.amazonaws.com/0/42614/bb9d7134-c722-3a56-c501-ba13ad1406d5.gif)


## ライフサイクル

### ウィンドウサイズ変更時のライフサイクル

Android 7.0(API level 24)のマルチウィンドウの標準的なライフサイクルと同様。

画面回転時のライフサイクルと同様に処理され、画面の再構成(onCreate)が行われます。

### ウィンドウ切替時のライフサイクル

Android 7のマルチウィンドウの標準的なライフサイクルと同様。

マルチ ウィンドウ モードでは、ユーザーが直前に操作したアクティビティのみが任意の時点でアクティブになる。このアクティビティは、トップレベルにあると見なされ、他のすべてのアクティビティは、表示されていても一時停止状態(onPause)になります。
ただし、一時停止状態ではあるが、表示されているこれらのアクティビティには、表示されていないアクティビティよりも高い優先度が付与されます。 ユーザーが一時停止状態のアクティビティのいずれかを操作した場合、そのアクティビティが再開(onResume呼び出し)され、前のトップレベルのアクティビティが一時停止(onPause呼び出し)します。

　　　 画面Bにフォーカス変更 | 画面Aの中身をクリック
画面A：onPause（優先度：高） -> onResume 
画面B：onResume -> onPause（優先度：高）
画面C（非表示）：onPause（優先度：低）


動画アプリなどの場合は、onPauseで各種のリソースを破棄している場合はマルチウィンドウの場合だけonStop() で動画を一時停止し、onStartで再生を再開するようにするなどの検討が必要です。

```
Activity.isInMultiWindowMode()
Fragment.isInMultiWindowMode()
```

## ウィンドウサイズ変更時のアニメーション

### 画面切り替え時のアニメーション

ウィンドウサイズ変更に合わせて、別々のレイアウトを表示することが多いですが、そのままではレイアウトがただ切り替わるだけで、画面変更が頻繁に発生するChromebookでは見栄えがよくありません。
自力でのViewのアニメーション実装やレイアウト切り替えは複雑で難しいため、ConstraintLayout2から使用できるConstraintLayoutStateを使用した画面変更時のアニメーションのサポートについて書きます。

0. ConstraintLayoutのバージョンを2にアップデート※まだ、beta段階
1. レイアウトファイルの修正
2. `constraint_states.xml` ファイルの作成
3. ConstraintLayoutの設定と、ステート変更のハンドリング
4. AndroidManifestへの画面変更時の設定追加

#### 0. レイアウトファイルの修正

`app/build.gradle`のdependencies内の、constraintlayoutのバージョンを変更する。

```gradle
dependencies {
    implementation "androidx.constraintlayout:constraintlayout:2.0.0-alpha2"
}
```

#### 1. レイアウトファイルの修正

以下のように、画面サイズなどの条件によってフォルダ分けされているレイアウトファイルを、すべてlayoutフォルダ内に入れます。

- /layout/activity_main.xml
- /layout-land/activity_main.xml -> /layout/activity_main_land.xml
- /layout-w400/activity_main.xml -> /layout/activity_main_w400.xml
- /layout-w600-land/activity_main.xml -> /layout/activity_main_w600_land.xml

※アニメーションさせたいレイアウト要素が、ConstraintLayoutで書かれていない場合はConstraintLayoutへの書き換えが必要。
※layoutのRoot要素がConstraintLayoutである必要はなく、画面内の一部のConstraintLayoutに対して適用することも可能です。

#### 2. `constraint_states.xml` ファイルの作成

layoutフォルダによる各レイアウトの適用をやめたので、 `ConstraintLayoutStates` によって各Viewの状態とlayoutファイルを紐づけます。
`res/xml/`フォルダに、`constraint_states.xml`ファイルを作成します。

```xml
<?xml version="1.0" encoding="utf-8"?>
<ConstraintLayoutStates xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <!-- 縦画面 -->
    <State
        android:id="@+id/constraintStatePortrait"
        app:constraints="@layout/activity_main">
        <!-- 縦画面 x 横幅が395db以下 => スマホの縦画面サイズ -->
        <Constraints
            app:constraints="@layout/activity_main"
            app:region_widthLessThan="395dp" />
        <!-- 縦画面 x 横幅が400db以上 => タブレットの縦画面サイズ -->
        <Constraints
            app:constraints="@layout/activity_main_w400"
            app:region_widthMoreThan="400dp" />
    </State>
    <!-- 横画面 -->
    <State
        android:id="@+id/constraintStateLandscape"
        app:constraints="@layout/activity_main_land">
        <!-- 横画面 x 横幅が595db以下 => スマホの横画面サイズ -->
        <Constraints
            app:constraints="@layout/activity_main_land"
            app:region_widthLessThan="595dp" />
        <!-- 横画面 x 横幅が600db以下 => タブレットの横、PCのフルスクリーンサイズ -->
        <Constraints
            app:constraints="@layout/activity_main_w600_land"
            app:region_widthMoreThan="600dp" />
    </State>
</ConstraintLayoutStates>
```

#### 3. ConstraintLayoutの設定と、ステート変更のハンドリング

Viewのセットアップ

```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // 先ほど作成したConstraintLayoutStatesのxmlを指定する
    constraintMain.setLayoutDescription(R.xml.constraint_states)
    // stateの変更に合わせて処理を行う場合は、setOnConstraintsChangedを使用することで変更イベントを受け取れる
    constraintMain.setOnConstraintsChanged(object : ConstraintsChangedListener() {
        override fun preLayoutChange(state: Int, layoutId: Int) {
            // レイアウト変更適用前の設定を記述
            val changeBounds = ChangeBounds().apply {
                duration = 600
                interpolator = AnticipateOvershootInterpolator(0.2f)
            }
            TransitionManager.beginDelayedTransition(constraintMain, changeBounds)
            when (layoutId) {
                R.layout.activity_main -> {
                    val reviewLayoutManager = LinearLayoutManager(baseContext, LinearLayoutManager.VERTICAL, false)
                    recyclerReviews.layoutManager = reviewLayoutManager
                }
                R.layout.activity_main_land -> {
                    val reviewLayoutManager = GridLayoutManager(baseContext, 2)
                    recyclerReviews.layoutManager = reviewLayoutManager
                }
                R.layout.activity_main_w400 -> {
                    val reviewLayoutManager = LinearLayoutManager(baseContext, LinearLayoutManager.VERTICAL, false)
                    recyclerReviews.layoutManager = reviewLayoutManager
                }
                R.layout.activity_main_w600_land -> {
                    val reviewLayoutManager = GridLayoutManager(baseContext, 2)
                    recyclerReviews.layoutManager = reviewLayoutManager
                }
            }
        }
        override fun postLayoutChange(stateId: Int, layoutId: Int) {
            // レイアウトの変更を適用するために、requestLayoutを呼び出し
            constraintMain.requestLayout()
        }
    })
    
    configurationUpdate(resources.configuration)
}

// 画面回転によるレイアウト変更を行う場合
override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    configurationUpdate(newConfig)
}

private fun configurationUpdate(configuration: Configuration) {
    if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        // 自分で特定のsetStateのレイアウトを指定することもできる
        constraintMain.setState(R.id.constraintStateLandscape, configuration.screenWidthDp, configuration.screenHeightDp)
    } else {
        constraintMain.setState(R.id.constraintStatePortrait, configuration.screenWidthDp, configuration.screenHeightDp)
    }
}
```

#### 4. AndroidManifestへの画面変更時の設定追加

AndroidManifestで、該当のactivityタグに`configChanges`を指定します。

```xml
<activity 
    android:name=".MainActivity"
    android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout">
```

---

※ConstraintStateでは、ConstraintLayout内のConstraintを更新するため、画面内の要素を変更することができないことに注意してください。

### サイズ変更時の描画遅延時の色指定を行う

ユーザーがウィンドウのサイズを変更した際に、ユーザー操作に追従するようにアクティビティのサイズ変更が行われます。
その際に、アプリで新しく表示された領域を描画するまでに時間がかかる場合、`windowBackground` 属性またはデフォルトの `windowBackgroundFallback` システム属性によって指定された色でこれらの領域が一時的に塗りつぶされます。

そのため、色差が大きい場合はActivityの背景色や、アプリのテーマカラーなどを指定しておくことをオススメします。

### メッセージ表示

エラーメッセージなどの表示にDialog、Toastなど複数の方法があるが、画面サイズ変更時に追従するかどうかなどはそれぞれ異なるので注意。

|      | リサイズ対応 | その他 |
|:-----|:----------:|:-----:|
| PupupWindow | ☓ | 表示後のサイズ変更は自分で行う必要がある |
| Toast | △ | 端末の画面を基準として、表示が行われる。固定サイズ |
| Dialog | ○ | 画面内に表示され、リサイズにもしっかりと追従する |

## ウィンドウ対応周りのAndroidデバッグ設定

開発者向けオプション > アプリ項目の各設定を有効化します。

- タイトルにデバッグ情報を表示する
- アクティビティサイズを変更可能にする
- 枠線のドラッグによるウィンドウの自由なサイズ調整を許可
- API レベル24以上のすべてのアプリケーションで自由形式のサイズ変更を有効にする
- 画面の向きが固定されたアプリのサイズ変更

![chromebook_android_debug](https://github.com/Reyurnible/chromebook-android-sample/raw/master/images/chromebook_android_debug.png)

# 入力サポート

Chromebookの入力周りのサポートについて

## 入力ソース

![input_method](https://github.com/Reyurnible/chromebook-android-sample/raw/master/images/input_method.png)

- タッチ
- スタイラスペン
- トラックパッド
- マウス
- キーボード

## トラックパッド/マウスサポート

### マウスカーソルサポート

Android 7.0 から Custom Pointer API が追加された。
基本的な動作については、実装しなくてもButtonのクリックや、TextViewのテキスト選択など大抵のことはデフォルトでカーソルが指定されています。

自分でポインターアイコンを設定する場合は、それぞれViewクラスに設定します。

- `View.setPointerIcon()`にIconをセットする。
- `View.onResolvePointerIcon()`をオーバーライドする。

ポインターアイコンは、デフォルトで用意されているシステムアイコンを使用するか、オリジナルのアイコンを指定することができます。

#### デフォルトのシステムアイコンを指定する場合

```xml
android:pointerIcon="hand"
```

or

```kotlin
val icon = PointerIcon.getSystemIcon(application, PointerIcon.TYPE_HAND)
view.pointerIcon = icon
```

##### デフォルトのシステムアイコンの種類 

- PointerIcon.TYPE_ARROW
- PointerIcon.TYPE_CONTEXT_MENU
- PointerIcon.TYPE_HAND
- PointerIcon.TYPE_HELP
- PointerIcon.TYPE_WAIT
- PointerIcon.TYPE_CELL
- PointerIcon.TYPE_CROSSHAIR
- PointerIcon.TYPE_TEXT
- PointerIcon.TYPE_VERTICAL_TEXT
- PointerIcon.TYPE_ALIAS
- PointerIcon.TYPE_COPY
- PointerIcon.TYPE_NO_DROP
- PointerIcon.TYPE_ALL_SCROLL
- PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW
- PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW
- PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW
- PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW
- PointerIcon.TYPE_ZOOM_IN
- PointerIcon.TYPE_ZOOM_OUT
- PointerIcon.TYPE_GRAB
- PointerIcon.TYPE_GRABBING

#### オリジナルのアイコンを使用する場合

pointer-iconタグのxmlファイルを、xmlフォルダの下などに追加する。

例：`res/xml/custom_pointer.xml`

```xml
<?xml version="1.0" encoding="utf-8"?>
<pointer-icon xmlns:android="http://schemas.android.com/apk/res/android"
              android:bitmap="@mipmap/ic_launcher"
              android:hotSpotX="24px"
              android:hotSpotY="24px"/>
```

それを、UIロジック側で読み込む。

```kotlin
val icon = PointerIcon.load(application.resources, R.xml.custom_pointer))
view.pointerIcon = icon
```

### 右クリックサポート

ラップトップだと、longClickListenerなどの代わりに右クリックでのイベントハンドリングを行う。

```kotlin
view.setOnContextClickListener {
    true
}
```

```kotlin
registerForContextMenu(view)

override fun onCreateContextMenu(menu: ContextMenu?, v: View?, menuInfo: ContextMenu.ContextMenuInfo?) {
    super.onCreateContextMenu(menu, v, menuInfo)
    val inflater = activity?.menuInflater ?: return
    when (v?.id) {
        R.id.menu_button -> {
            // Create cart product context menu
            inflater.inflate(R.menu.shr_cart_product_context_menu, menu)
        }
    }
}

override fun onContextItemSelected(item: MenuItem?): Boolean {
    // Action selected menu item.
    return true
}
```

## キーボードサポート

Chromebookではキーボードのサポートとして、タブキーによるフォーカス変更、ショートカットキーによる操作を推奨してます。

### タブキー、方向キーサポート

- タブによるnextフォーカスの設定
- 方向キーによるそれぞれのフォーカスの設定

#### Focusable設定

```xml
android:focusable="true"
```

or

```kotlin
view.setFocusable = true
```

また、必要に応じて背景などのリソースをFocusableなものにしておきます。

#### タブキーサポート

```xml
android:nextFocusForward="@id/next_view"
```

```kotlin
view.nextFocusForwardId = R.id.next_view
```

#### 方向キーサポート

```xml
android:nextFocusLeft="@id/view_to_left"
android:nextFocusRight="@id/view_to_right"
android:nextFocusUp="@id/view_above"
android:nextFocusDown="@id/view_below"
```

or

```kotlin
view.nextFocusLeftId = R.id.view_to_left
view.nextFocusRightId = R.id.view_to_right
view.nextFocusTopId = R.id.view_above
view.nextFocusBottomId = R.id.view_below
```

### キーボードナビゲーションクラスタ

Android 8.0から、ViewGroup単位でタブキーのハンドリングを行う方法が追加されました。

画面の構成を以下の図のように、ナビゲーションクラスタ（タブキーで移動させたいグループ）ごとにまとめます。

![keyboard_cluster](https://github.com/Reyurnible/chromebook-android-sample/raw/master/images/keyboard_cluster.png)

まとめた各クラスタのrootのViewにだけ、`keyboardNavigationCluster`プロパティを指定します。

```xml
android:keyboardNavigationCluster="true"
```

or

```kotlin
view.setKeyboardNavigationCluster = true
```

※ネストされていないクラスタが階層の別のレベルに表示されることがあっても、クラスタはネストできません。クラスタをネストしようとすると、フレームワークは最上位の ViewGroup 要素のみをクラスタとして処理します。

つまり、ConstraintLayoutなどで、全クラスタがフラットな状態になるようにViewを組む必要があります。

### ショートカットキーサポート

アプリ内でよくある操作を、ショートカットとしてサポートします。
`Activity.dispatchKeyShortcutEvent`をオーバーライドすることによって、キーを押された際のイベントをハンドリングすることができます。

```kotlin
override fun dispatchKeyShortcutEvent(event: KeyEvent?): Boolean {
    if (event?.keyCode == KeyEvent.KEYCODE_Z &&
        event.hasModifiers(KeyEvent.META_CTRL_ON)) {
        // Ctrl + z => undo
        viewModel.onUndoKeyShortcut()
        return true
    } else if (event?.keyCode == KeyEvent.KEYCODE_Z &&
        event.hasModifiers(KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON)) {
        // Ctrl + Shift + z => redo
        viewModel.onRedoKeyShortcut()
        return true
    } else {
        return false
    }
}
```

### デフォルトで有効になっているキーボード操作

- `Escキー`と`F1キー`は、スマホのバックボタンと同じ挙動になります。

## 疑似タッチスクリーンの許可

Chrome OS バージョン M53 以降では、android.hardware.touchscreen 機能を明示的に要求しないすべての Android アプリが、android.hardware.faketouch 機能をサポートする Chrome OS デバイスでも機能するようになりました。そのため、何も対応する必要はありません。
逆に、Chrome OS バージョン M52 以下では、デフォルトでAndroidアプリは android.hardware.touchscreen機能を必要とするため、タッチスクリーンがないChromebookだとインストールできないため、疑似タップインターフェースを備えた端末でもアプリを使用できるようにしたい場合はタッチスクリーンが必須ではないことを明示的に宣言する必要があります。

```xml
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
```

疑似タップインターフェースを備えたデバイスは、基本的なタップイベントをエミュレートする入力システムをユーザーに提供します。
たとえばユーザーは、マウスまたはリモコンを操作して、画面上のカーソルの移動、リストのスクロール、画面の一部から別の部分への要素のドラッグなどを行うことができます。

# ChromeOS エミュレーターでのデバッグ方法



参考サイト：https://developer.android.com/topic/arc/emulator?hl=ja

## 別PCからのDebugについて

外部PCからChromebookに対して、adb接続してアプリをインストールしデバッグする方法です。
初期の状態は、ノーマルモードとなっており外部のPCからのファイル書き込みや、通信などなにもできない状況です。

### 主な手順

- ChromeOSの開発者モード化
- GooglePlayの利用規約に同意
- Android設定の開発者モード化
- ADBデバッグの許可
- 外部PCのAndroidStudioからの接続
    - ChromeBookのIPアドレスの確認
    - AndroidStudioでの確認

### ChromeOSの開発者モード化

開発者モードに切り替えると、 "root" shellへのアクセスができるようになります。
ただし、切り替えると*端末が再起動し、端末上のすべてのデータが消えます。*
また、ノーマルモードに戻す際にも同じく*端末が再起動し、端末上のすべてのデータが消えます。*
PC内にあるデータは、GoogleDriveなどにしっかりと保存しておきましょう。

参考サイト：https://www.chromium.org/chromium-os/poking-around-your-chrome-os-device


#### リカバリーモード

#### デベロッパーモード

各起動後の注意として、毎回の起動時には**Ctrl + D**を押して起動します。
でないと、ノーマルモードでの起動となりすべてのデータが消えます。

#### ノーマルモードへの切り替え方法

再起動後、**Spaceキー** を押して起動します。
ノーマルモードへの切り替え時もデータが消えるため、注意してください。

### Android設定の開発者モード化

ADB デバッグを有効にするには、次のステップを実行します。

1. 画面の右下部にあるクロック アイコンをクリックします。
1. [Settings] をクリックします。
1. [Android Apps] セクションで、[Manage your Android apps in Settings] という行にある [Settings] リンクをクリックします。これにより、Android アプリの設定が表示されます。
1. [About device] をクリックします。
1. [Build number] を 7 回クリックして、デベロッパー モードに移行します。
1. ウィンドウの左上部にある矢印をクリックして、メインの [Settings] 画面に戻ります。
1. 新しい [Developer options] アイテムをクリックし、[ADB debugging] をアクティブにし、[OK] をクリックして、ADB デバッグを有効にします。

### 外部PCのAndroidStudioからの接続

別のPCから、ChromebookにUSB接続してAndroidStudioでデバイス確認を行うと `100.115.92.2:5555` が表示されるがそれは外部PCからは接続できないので注意。

#### ChromebookのIPを確認する

ChromeOS情報のchronosユーザーのターミナル（`ctrl+alt+T` + shell）上で、「 **ip -4 a** 」と入力。

```terminal
chronos@localhost / $ ip -4 a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: arcbr0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 100.115.92.1/30 brd 100.115.92.3 scope global arcbr0
       valid_lft forever preferred_lft forever
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    inet 192.168.128.100/24 brd 192.168.128.255 scope global wlan0
       valid_lft forever preferred_lft forever
6: vmtap0: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 1000
    inet 100.115.92.5/30 brd 100.115.92.7 scope global vmtap0
       valid_lft forever preferred_lft forever
```

一番下の、vmtap0のIPの**5555**番ポートで別のPCから `adb connect` できる。

```terminal
$adb connect 192.168.0.32:5555
```

あとは、いつも通り起動確認のダイアログがでるのでOKをクリックする。

# ChromeOS上でのAndroidアプリの開発

## 主な手順

- ChromeOSの開発者モード化
- GooglePlayの利用規約に同意
- Android設定の開発者モード化
- ADBデバッグの許可
- （↑までは、[ChromeOSでのアプリデバッグ方法](https://github.com/Reyurnible/chromebook-android-sample/wiki/ChromeOS%E3%81%A7%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AA%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E6%96%B9%E6%B3%95)を参照）
- Linuxの有効化
- AndroidStudioのインストール
- ファイアウォールの設定
- 端末のAndroidStudioからの接続

## Linuxの有効化

参考サイト：https://developer.android.com/topic/arc/studio

## AndroidStudioのインストール 

ChromeOSでのAndroidStudioのインストール

AndroidStudioの起動

「 **./android-studio/bin/studio.sh** 」

## ファイアウォール設定

1. Chromeブラウザ上で、 **Ctrl+Alt+T** を押してChrome OS端末を起動する
1. 「 **shell** 」と入力して、bash コマンドシェルを開始する

```terminal
crosh> shell
chronos@localhost / $
```

## 端末のAndroidStudioからの接続

Linuxの `ターミナル` アプリを起動。

```terminal
{username}@penguin adb kill-server
{username}@penguin connect_adb
```

`connect_adb` は、AndroidStudioのインストール時に.bashrcに、以下のように登録されている関数。

```terminal
function connect_adb() {
    adb connect 100.115.92.2:5555
}
```

# 参考リンク

## 情報サイト

- [Chrome Unboxed](https://chromeunboxed.com/)
- [StackOverflow](https://stackoverflow.com/questions/tagged/androidappsonchromeos)

## Androidアプリの作り方について

- [Chrome OS デバイス](https://developer.android.com/chrome-os/?hl=ja)
- [Apps for Chrome OS overview](https://developer.android.com/chrome-os/intro)
- [Chromebook 向けアプリの最適化](https://developer.android.com/topic/arc/)
- [Get Your Android App Ready for Chromebooks](https://medium.com/androiddevelopers/get-your-android-app-ready-for-chromebooks-b4915426d021)
- [Window management](https://developer.android.com/topic/arc/window-management)
- [マルチ ウィンドウのサポート](https://developer.android.com/guide/topics/ui/multi-window)
- [google/rally](https://github.com/google/rally)
- [New Features in ConstraintLayout 2.0](http://androidkt.com/new-features-in-constraintlayout-2-0/)
- [Responsive layout grid](https://material.io/design/layout/responsive-layout-grid.html#grid-behavior)
- [入力とナビゲーション](https://developer.android.com/about/versions/oreo/android-8.0?hl=ja#ian)
- [Chromebook 向けアプリ マニフェストの互換性](https://developer.android.com/topic/arc/manifest?hl=ja)
- [AndroidNのDropPermissionsを使ってアプリケーション間でDragAndDropする](http://funnelbit.hatenablog.com/entry/2016/04/01/092611)

### Google I/O

- [What’s new in Android apps for Chrome OS (Google I/O '18)](https://www.youtube.com/watch?v=6kIZ_u4QE9U)
- [Android Apps for Chromebooks and Large Screen Devices (Google I/O '17)](https://www.youtube.com/watch?v=v6QH89i4YCI)

### Google Codelabs

- [ChromeOS Resizing](https://codelabs.developers.google.com/codelabs/chromeos-resizing/index.html?index=..%2F..index#0)
- [Optimized for ChromeOS](https://codelabs.developers.google.com/codelabs/optimized-for-chromeos/index.html?index=..%2F..index#0)

## セットアップ

### 開発者モード

- [Chromebook デベロッパー（開発者）モードにする方法](http://take-root.net/gadgets/chromebook-developer-mode/)
- [Chromebook デベロッパーモードの入り方と出方。](http://usagisaigon.blogspot.jp/2013/08/chromebook_11.html)

### Androidの開発環境

- [Chromebook 向けアプリの最適化 セットアップ](https://developer.android.com/topic/arc/#setup)
- [Chromebookで動作するAndroidアプリにadbを接続する方法](https://qiita.com/jiro-aqua/items/81c2a35225387c436f2f)
- [ChromebookでAndroidアプリをデバッグする方](https://qiita.com/kurun_pan/items/252487246bc45d986b97)
- [How to configure your Chromebook to use ADB](https://www.utest.com/articles/how-to-configure-your-chromebook-to-use-adb) 
- [Connect Android USB Devices in Chrome OS](https://medium.com/@putraxor/connect-android-usb-devices-in-chrome-os-41bb4f9fc915)
- [Android Container Build](https://chromium.googlesource.com/chromiumos/platform2/+/master/arc/container-bundle/)

### その他

- [Chromebook のキーボード ショートカット](https://support.google.com/chromebook/answer/183101?hl=ja)